Merge branch '0.6.4' into future
authorSofia Papagiannaki <papagian@gmail.com>
Fri, 2 Nov 2012 12:22:43 +0000 (14:22 +0200)
committerSofia Papagiannaki <papagian@gmail.com>
Fri, 2 Nov 2012 12:22:43 +0000 (14:22 +0200)
Conflicts:
snf-astakos-app/astakos/im/synnefo_settings.py

54 files changed:
1  2 
.gitignore
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/backends/__init__.py
snf-astakos-app/astakos/im/api/backends/base.py
snf-astakos-app/astakos/im/api/backends/lib/__init__.py
snf-astakos-app/astakos/im/api/backends/lib/django/__init__.py
snf-astakos-app/astakos/im/api/callpoint.py
snf-astakos-app/astakos/im/api/client.py
snf-astakos-app/astakos/im/api/spec.py
snf-astakos-app/astakos/im/context_processors.py
snf-astakos-app/astakos/im/endpoints/aquarium/consumer.py
snf-astakos-app/astakos/im/endpoints/quotaholder.py
snf-astakos-app/astakos/im/forms.py
snf-astakos-app/astakos/im/functions.py
snf-astakos-app/astakos/im/management/commands/group-permissions-add.py
snf-astakos-app/astakos/im/management/commands/group-permissions-remove.py
snf-astakos-app/astakos/im/management/commands/invitation-details.py
snf-astakos-app/astakos/im/management/commands/quotaholder-sync.py
snf-astakos-app/astakos/im/management/commands/resource-add.py
snf-astakos-app/astakos/im/management/commands/service-token-renew.py
snf-astakos-app/astakos/im/management/commands/user-activation-send.py
snf-astakos-app/astakos/im/management/commands/user-add.py
snf-astakos-app/astakos/im/management/commands/user-details.py
snf-astakos-app/astakos/im/management/commands/user-invite.py
snf-astakos-app/astakos/im/management/commands/user-modify.py
snf-astakos-app/astakos/im/migrations/0017_populate_resource_data.py
snf-astakos-app/astakos/im/migrations/0020_auto__chg_field_astakosgroup_homepage.py
snf-astakos-app/astakos/im/migrations/0021_auto__add_field_astakosgroupquota_uplimit__add_field_astakosuserquota_.py
snf-astakos-app/astakos/im/migrations/0022_copy_limit_to_uplimit.py
snf-astakos-app/astakos/im/migrations/0023_populate_resource_data.py
snf-astakos-app/astakos/im/migrations/0024_auto__chg_field_astakosgroupquota_lim.py
snf-astakos-app/astakos/im/migrations/0025_case_insensitive_emails.py
snf-astakos-app/astakos/im/migrations/0026_auto__add_field_resource_desc__add_field_resource_unit.py
snf-astakos-app/astakos/im/migrations/0027_auto__add_field_astakosuser_dirsturbed_quota.py
snf-astakos-app/astakos/im/migrations/0028_auto__add_field_resource_group.py
snf-astakos-app/astakos/im/models.py
snf-astakos-app/astakos/im/settings.py
snf-astakos-app/astakos/im/static/im/css/modules.css
snf-astakos-app/astakos/im/static/im/js/common.js
snf-astakos-app/astakos/im/synnefo_settings.py
snf-astakos-app/astakos/im/target/shibboleth.py
snf-astakos-app/astakos/im/templates/im/astakosuserquota_list.html
snf-astakos-app/astakos/im/templates/im/resource_list.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
snf-astakos-app/distribute-0.6.10-py2.6.egg
snf-astakos-app/distribute-0.6.10.tar.gz
snf-astakos-app/setup.py

diff --cc .gitignore
@@@ -6,3 -6,3 +6,5 @@@ docs/buil
  .project
  .pydevproject
  snf-astakos-app/astakos/version.py
++snf-astakos-app/distribute-0.6.10-py2.6.egg
++snf-astakos-app/distribute-0.6.10.tar.gz
@@@ -85,7 -85,7 +85,10 @@@ ASTAKOS_INVITATION_EMAIL_SUBJEC
  ASTAKOS_GREETING_EMAIL_SUBJECT              'Welcome to %s alpha2 testing' % SITENAME                                       Welcome email subject
  ASTAKOS_FEEDBACK_EMAIL_SUBJECT              'Feedback from %s alpha2 testing' % SITENAME                                    Feedback email subject
  ASTAKOS_VERIFICATION_EMAIL_SUBJECT          '%s alpha2 testing account activation is needed' % SITENAME                     Account activation email subject
--ASTAKOS_ADMIN_NOTIFICATION_EMAIL_SUBJECT    '%s alpha2 testing account created (%%(user)s)' % SITENAME                      Account creation admin notification email subject
++ASTAKOS_ACCOUNT_CREATION_SUBJECT = getattr(settings, 'ASTAKOS_ACCOUNT_CREATION_SUBJECT',
++        '%s alpha2 testing account created (%%(user)s)' % SITENAME)
++ASTAKOS_GROUP_CREATION_SUBJECT = getattr(settings, 'ASTAKOS_',
++        '%s alpha2 testing group created (%%(group)s)' % SITENAME)
  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
@@@ -38,8 -38,8 +38,9 @@@ from django.utils.translation import ug
  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_admin_notification, activate
++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
  
  import logging
@@@ -129,10 -129,10 +130,9 @@@ class ActivationBackend(object)
                      send_activation(user, activation_template_name)
                      return VerificationSent()
              else:
--                send_admin_notification(
++                send_account_creation_notification(
                      template_name=admin_email_template_name,
--                    dictionary={'user': user, 'group_creation': True},
--                    subject='%s alpha2 testing account notification' % SITENAME
++                    dictionary={'user': user, 'group_creation': True}
                  )
                  return NotificationSent()
          except BaseException, e:
@@@ -173,33 -173,33 +173,33 @@@ def get_menu(request, with_extra_links=
          item = MenuItem
          item.current_path = absolute(request, request.path)
          append(item(
--                url=absolute(request, reverse('index')),
--                name=user.email))
++               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"))
++                       url=absolute(request, reverse('password_change')),
++                       name="Change password"))
              if EMAILCHANGE_ENABLED:
                  append(item(
--                        url=absolute(request, reverse('email_change')),
--                        name="Change email"))
++                       url=absolute(request, reverse('email_change')),
++                       name="Change email"))
              if INVITATIONS_ENABLED:
                  append(item(
--                        url=absolute(request, reverse('invite')),
--                        name="Invitations"))
++                       url=absolute(request, reverse('invite')),
++                       name="Invitations"))
              append(item(
--                    url=absolute(request, reverse('feedback')),
--                    name="Feedback"))
++                   url=absolute(request, reverse('feedback')),
++                   name="Feedback"))
              append(item(
--                    url=absolute(request, reverse('group_list')),
--                    name="Groups",
--                    submenu=(item(
--                                url=absolute(request,
--                                             reverse('group_list')),
--                                name="Overview"),
++                   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')),
                                               reverse('group_search')),
                                  name="Join"),)))
              append(item(
--                    url=absolute(request, reverse('resource_list')),
--                    name="Resources"))
++                   url=absolute(request, reverse('resource_list')),
++                   name="Resources"))
              append(item(
--                    url=absolute(request, reverse('billing')),
--                    name="Billing"))
++                   url=absolute(request, reverse('billing')),
++                   name="Billing"))
              append(item(
--                    url=absolute(request, reverse('timeline')),
--                    name="Timeline"))
++                   url=absolute(request, reverse('timeline')),
++                   name="Timeline"))
          if with_signout:
              append(item(
--                    url=absolute(request, reverse('logout')),
--                    name="Sign out"))
++                   url=absolute(request, reverse('logout')),
++                   name="Sign out"))
  
      callback = request.GET.get('callback', None)
      data = json.dumps(tuple(l))
index 0000000,0000000..93314df
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,38 @@@
++# 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.backends.lib.django import DjangoBackend
++
++
++def get_backend():
++    return DjangoBackend()
index 0000000,0000000..080634f
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,18 @@@
++class ItemNotExists(NameError):
++    pass
++
++
++class ItemExists(NameError):
++    pass
++
++
++class MissingIdentifier(IOError):
++    pass
++
++
++class BaseBackend(object):
++    def update_user():
++        pass
++
++    def create_user():
++        pass
index 0000000,0000000..e69de29
new file mode 100644 (file)
--- /dev/null
--- /dev/null
index 0000000,0000000..37c6352
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -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, Resource, Service, RESOURCE_SEPARATOR
++from astakos.im.api.backends.base import (BaseBackend, ItemNotExists,
++                                          ItemExists, MissingIdentifier)
++from astakos.im.util import reserved_email, model_to_dict
++from astakos.im.endpoints.quotaholder import get_quota
++
++import logging
++
++logger = logging.getLogger(__name__)
++
++DEFAULT_CONTENT_TYPE = None
++
++
++def safe(propagate_exceptions=False):
++    """Decorator function for views that implement an API method."""
++
++    def decorator(func):
++        @transaction.commit_manually
++        @wraps(func)
++        def wrapper(self, *args, **kwargs):
++            logger.debug('%s %s %s' % (func, args, kwargs))
++            try:
++                r = func(self, *args, **kwargs) or ()
++            except Exception, e:
++                logger.exception(e)
++                transaction.rollback()
++                if propagate_exceptions:
++                    raise e
++                else:
++                    args = list(args)
++                    args.append(e)
++                    r = args
++            else:
++                transaction.commit()
++            r = filter(bool, r) # filter out None elements
++            return list(r)
++        return wrapper
++    return decorator
++
++
++class DjangoBackend(BaseBackend):
++    def _lookup_object(self, model, **kwargs):
++        """
++        Returns an object of the specific model having this id.
++        """
++        if not kwargs:
++            raise MissingIdentifier
++        try:
++            return model.objects.get(**kwargs)
++        except model.DoesNotExist:
++            raise ItemNotExists()
++
++    def _lookup_user(self, id):
++        """
++        Returns an AstakosUser having this id.
++        """
++        return self._lookup_object(AstakosUser, id=id)
++
++    def _lookup_service(self, id):
++        """
++        Returns an Service having this id.
++        """
++        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(**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
++
++    @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))
++        if rejected:
++            raise Exception(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))
++        if rejected:
++            raise Exception(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))
++        if rejected:
++            raise Exception(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))
++        if rejected:
++            raise Exception(rejected)
++
++    @safe()
++    def invite_users(self, senderid, recipients=()):
++        user = self._lookup_user(senderid)
++        rejected = []
++        append = rejected.append
++        for r in recipients:
++            try:
++                user.invite(r.get('email'), r.get('realname'))
++            except (IntegrityError, SMTPException), e:
++                append((email, e))
++        if rejected:
++            raise Exception(rejected)
++
++    @safe(propagate_exceptions=True)
++    def list_users(self, filter=()):
++        return self._list(AstakosUser, filter=filter)
++
++    @safe(propagate_exceptions=True)
++    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.name if resource.unit else '',
++                     maxValue=quantity + capacity,
++                     currValue=quantity + imported - released - exported + returned)
++            append(d)
++        return resources
++
++    @safe(propagate_exceptions=True)
++    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
++
++    @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))
++        if rejected:
++            raise Exception(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
index 0000000,0000000..31711dc
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,162 @@@
++# 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 backends import get_backend
++
++from commissioning import (Callpoint,
++                           #                             CommissionException,
++                           #                             CorruptedError, InvalidDataError,
++                           #                             InvalidKeyError, NoEntityError,
++                           #                             NoQuantityError, NoCapacityError,
++                           #                             ExportLimitError, ImportLimitError
++                           )
++
++
++# 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)
++
++class AstakosDjangoDBCallpoint():
++
++    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 = AstakosDjangoDBCallpoint
index 0000000,0000000..192b38b
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,11 @@@
++#!/usr/bin/env python
++from commissioning.clients.http import main, HTTP_API_Client
++from astakos.im.api.spec import AstakosAPI
++
++
++class AstakosHTTP(HTTP_API_Client):
++    api_spec = AstakosAPI()
++
++
++if __name__ == '__main__':
++    main(callpoint=AstakosHTTP())
index 0000000,0000000..f30e251
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,421 @@@
++from commissioning.api.specificator import (
++    CanonifyException, SpecifyException,
++    Specificator, Null, Integer, Text,
++    Tuple, ListOf, Dict, Args)
++
++
++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(),
++                estimated_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=GroupKind,
++            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=GroupKind,
++            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=GroupKind,
++            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):
++        return Text()
++
++    def add_approval_terms(location=Filepath):
++        return Nonnegative
++
++#     def change_emails():
++#         pass
@@@ -74,8 -74,8 +74,8 @@@ def custom_messages(request)
      if type(PROFILE_MESSAGES) == dict:
          PROFILE_MESSAGES = PROFILE_MESSAGES.items()
  
--    EXTRA_MESSAGES_SET = bool(GLOBAL_MESSAGES or SIGNUP_MESSAGES or \
--            LOGIN_MESSAGES or PROFILE_MESSAGES)
++    EXTRA_MESSAGES_SET = bool(GLOBAL_MESSAGES or SIGNUP_MESSAGES or
++                              LOGIN_MESSAGES or PROFILE_MESSAGES)
  
      return {
          'GLOBAL_MESSAGES': GLOBAL_MESSAGES,
@@@ -34,8 -34,8 +34,8 @@@
  import logging
  
  logging.basicConfig(format='%(asctime)s [%(levelname)s] %(name)s %(message)s',
--        datefmt='%Y-%m-%d %H:%M:%S'
--)
++                    datefmt='%Y-%m-%d %H:%M:%S'
++                    )
  logger = logging.getLogger('endpoint.aquarium')
  
  from astakos.im.models import AstakosUser
@@@ -57,19 -57,19 +57,19 @@@ def call(func_name)
          def wrapper(entities=(), client=None, **kwargs):
              if not entities:
                  return ()
--        
++
              if not QUOTA_HOLDER_URL:
                  return ()
--        
++
              c = client or QuotaholderHTTP(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,
--            
++                return c, data
++
              funcname = func.__name__
              kwargs = {'context': {}, funcname: data}
              rejected = func(**kwargs)
@@@ -79,6 -79,6 +79,7 @@@
          return wrapper
      return decorator
  
++
  @call('set_quota')
  def send_quota(users, client=None):
      data = []
@@@ -91,8 -91,8 +92,9 @@@
              import_limit = None
              export_limit = None
              flags = 0
--            args = (user.email, resource, key, quantity, capacity, import_limit,
--                    export_limit, flags)
++            args = (
++                user.email, resource, key, quantity, capacity, import_limit,
++                export_limit, flags)
              append(args)
      return data
  
@@@ -172,9 -172,9 +174,11 @@@ 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
      t['issue_time'] = before
      yield t
  
++
  def _usage_units(timeline, after, before, details=0):
  
      t_total = 0
              'total',
              point['resource'],
              issue_time,
--            uu_total/t_total,
++            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
              'total',
              point['resource'],
              issue_time,
--            tu_total//len(timeline),
++            tu_total // len(timeline),
              tu_total)
  
++
  def timeline_charge(entity, resource, after, before, details, charge_type):
      key = '1'
      if charge_type == 'charge_usage':
  
      quotaholder = QuotaholderHTTP(QUOTA_HOLDER_URL)
      timeline = quotaholder.get_timeline(
--                            context         =   {},
--                            after           =   after,
--                            before          =   before,
--                            get_timeline    =   [[entity, resource, key]])
++        context={},
++        after=after,
++        before=before,
++        get_timeline=[[entity, resource, key]])
      cu = charge_units(timeline, after, before, details=details)
      return cu
--
@@@ -53,8 -53,8 +53,8 @@@ from astakos.im.models import (AstakosU
  from astakos.im.settings import (INVITATIONS_PER_LEVEL, BASEURL, SITENAME,
                                   RECAPTCHA_PRIVATE_KEY, RECAPTCHA_ENABLED,
                                   DEFAULT_CONTACT_EMAIL, LOGGING_LEVEL,
-                                  PASSWORD_RESET_EMAIL_SUBJECT, 
 -                                 PASSWORD_RESET_EMAIL_SUBJECT)
 -
++                                 PASSWORD_RESET_EMAIL_SUBJECT,
 +                                 NEWPASSWD_INVALIDATE_TOKEN)
  from astakos.im.widgets import DummyWidget, RecaptchaWidget
  from astakos.im.functions import send_change_email
  
@@@ -176,7 -176,7 +176,7 @@@ class InvitedLocalUserCreationForm(Loca
          ro = ('email', 'username',)
          for f in ro:
              self.fields[f].widget.attrs['readonly'] = True
--    
++
      def save(self, commit=True):
          user = super(InvitedLocalUserCreationForm, self).save(commit=False)
          level = user.invitation.inviter.level + 1
@@@ -193,7 -193,7 +193,7 @@@ class ThirdPartyUserCreationForm(forms.
          model = AstakosUser
          fields = ("email", "first_name", "last_name",
                    "third_party_identifier", "has_signed_terms")
--    
++
      def __init__(self, *args, **kwargs):
          """
          Changes the order of fields, and removes the username field.
                  % (reverse('latest_terms'), _("the terms"))
              self.fields['has_signed_terms'].label = \
                  mark_safe("I agree with %s" % terms_link_html)
--    
++
      def clean_email(self):
          email = self.cleaned_data['email']
          if not email:
@@@ -273,7 -273,7 +273,7 @@@ class InvitedThirdPartyUserCreationForm
  class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
      additional_email = forms.CharField(
          widget=forms.HiddenInput(), label='', required=False)
--    
++
      def __init__(self, *args, **kwargs):
          super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
          self.fields.keyOrder.append('additional_email')
          field = self.fields[name]
          self.initial['additional_email'] = self.initial.get(name,
                                                              field.initial)
--    
++
      def clean_email(self):
          email = self.cleaned_data['email']
          for user in AstakosUser.objects.filter(email=email):
@@@ -305,7 -304,7 +305,7 @@@ class LoginForm(AuthenticationForm)
      recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
      recaptcha_response_field = forms.CharField(
          widget=RecaptchaWidget, label='')
--    
++
      def __init__(self, *args, **kwargs):
          was_limited = kwargs.get('was_limited', False)
          request = kwargs.get('request', None)
          if was_limited and RECAPTCHA_ENABLED:
              self.fields.keyOrder.extend(['recaptcha_challenge_field',
                                           'recaptcha_response_field', ])
--    
++
 +    def clean_username(self):
 +        if 'username' in self.cleaned_data:
 +            return self.cleaned_data['username'].lower()
-     
++
      def clean_recaptcha_response_field(self):
          if 'recaptcha_challenge_field' in self.cleaned_data:
              self.validate_captcha()
          if not check.is_valid:
              raise forms.ValidationError(
                  _('You have not entered the correct words'))
--    
++
      def clean(self):
          super(LoginForm, self).clean()
          if self.user_cache and self.user_cache.provider not in ('local', ''):
@@@ -524,12 -515,12 +524,9 @@@ class ExtendedPasswordChangeForm(Passwo
          super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
  
      def save(self, commit=True):
--        user = super(ExtendedPasswordChangeForm, self).save(commit=False)
 -        if self.cleaned_data.get('renew'):
 -            user.renew_token()
 -        if commit:
 -            user.save()
 -        return user
 +        if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
-             user.renew_token()
-         if commit:
-             user.save()
-         return user
++            self.user.renew_token()
++        return super(ExtendedPasswordChangeForm, self).save(commit=commit)
  
  
  class AstakosGroupCreationForm(forms.ModelForm):
      name = forms.URLField()
      moderation_enabled = forms.BooleanField(
          help_text="Check if you want to approve members participation manually",
--        required=False   
++        required=False
      )
--    
++
      class Meta:
          model = AstakosGroup
  
          except KeyError:
              resources = {}
          super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
--        self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc', 'issue_date',
--                                'expiration_date', 'estimated_participants',
++        self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
++                                'issue_date', 'expiration_date',
++                                'estimated_participants',
                                  'moderation_enabled']
          for id, r in resources.iteritems():
              self.fields['resource_%s' % id] = forms.IntegerField(
--                label=r,
--                required=False,
--                help_text=_('Leave it blank for no additional quota.')
--            )
++                label=r, required=False,
++                help_text=_('Leave it blank for no additional quota.'))
  
      def resources(self):
          for name, value in self.cleaned_data.items():
                      continue
                  yield (suffix, value)
  
++
  class AstakosGroupUpdateForm(forms.ModelForm):
      class Meta:
          model = AstakosGroup
          fields = ('homepage', 'desc')
  
++
  class AddGroupMembersForm(forms.Form):
--    q = forms.CharField(max_length=800, widget=forms.Textarea, label=_('Search users'),
--                        help_text=_('Add comma separated user emails'),
--                        required=True)
--    
++    q = forms.CharField(
++        max_length=800, widget=forms.Textarea, label=_('Search users'),
++        help_text=_('Add comma separated user emails'),
++        required=True)
++
      def clean(self):
          q = self.cleaned_data.get('q') or ''
          users = q.split(',')
                  _('Unknown users: %s' % ','.join(unknown)))
          self.valid_users = db_entries
          return self.cleaned_data
--    
++
      def get_valid_users(self):
          """Should be called after form cleaning"""
          try:
  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)
++        queryset=AstakosUser.objects.filter(is_active=True)
      )
      resource = forms.ModelChoiceField(
          queryset=Resource.objects.all()
      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'), )
--                )
++        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]
++            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:
++        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'),
++                                         ('expiration_date',
++                                          'Expiration Date'),
++                                         ('approved_members_num',
++                                          'Participants'),
                                           ('is_enabled', 'Status'),
                                           ('moderation_enabled', 'Moderation'),
--                                         ('membership_status','Enrollment Status')
++                                         ('membership_status',
++                                          'Enrollment Status')
                                           ),
                                  required=False)
  
++
  class MembersSortForm(forms.Form):
      sort_by = forms.ChoiceField(label='Sort by',
                                  choices=(('person__email', 'User Id'),
                                           ),
                                  required=False)
  
++
  class PickResourceForm(forms.Form):
      resource = forms.ModelChoiceField(
          queryset=Resource.objects.select_related().all()
      )
--    resource.widget.attrs["onchange"]="this.form.submit()"
++    resource.widget.attrs["onchange"] = "this.form.submit()"
++
 +
 +class ExtendedSetPasswordForm(SetPasswordForm):
 +    """
 +    Extends SetPasswordForm by enabling user
 +    to optionally renew also the token.
 +    """
 +    if not NEWPASSWD_INVALIDATE_TOKEN:
 +        renew = forms.BooleanField(label='Renew token', required=False,
 +                                   initial=True,
 +                                   help_text='Unsetting this may result in security risk.')
-     
++
 +    def __init__(self, user, *args, **kwargs):
 +        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
-     
++
 +    def save(self, commit=True):
-         user = super(ExtendedSetPasswordForm, self).save(commit=False)
 +        if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
-             try:
-                 user = AstakosUser.objects.get(id=user.id)
-             except AstakosUser.DoesNotExist:
-                 pass
-             else:
-                 user.renew_token()
-         if commit:
-             user.save()
-         return user
++            if isinstance(self.user, AstakosUser):
++                self.user.renew_token()
++        return super(ExtendedSetPasswordForm, self).save(commit=commit)
@@@ -52,13 -52,13 +52,13 @@@ from functools import wrap
  
  from astakos.im.settings import (DEFAULT_CONTACT_EMAIL, SITENAME, BASEURL,
                                   LOGGING_LEVEL, VERIFICATION_EMAIL_SUBJECT,
--                                 ADMIN_NOTIFICATION_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 AstakosUser
  
  logger = logging.getLogger(__name__)
  
@@@ -68,12 -68,12 +68,11 @@@ def logged(func, msg)
      def with_logging(*args, **kwargs):
          email = ''
          user = None
--        if len(args) == 2 and isinstance(args[1], AstakosUser):
--            user = args[1]
--        elif len(args) == 1 and isinstance(args[0], HttpRequest):
++        try:
              request = args[0]
--            user = request.user
--        email = user.email if user and user.is_authenticated() else ''
++            email = request.user.email
++        except (KeyError, AttributeError), e:
++            email = ''
          r = func(*args, **kwargs)
          if LOGGING_LEVEL:
              logger.log(LOGGING_LEVEL, msg % email)
@@@ -116,10 -116,10 +115,9 @@@ def send_activation(user, template_name
      user.save()
  
  
- def send_admin_notification(user, template_name,
-                             dictionary=None,
-                             subject='alpha2 testing notification',
-                             ):
 -def send_admin_notification(template_name,
 -                            dictionary=None,
 -                            subject='alpha2 testing notification',
 -                            ):
++def _send_admin_notification(template_name,
++                             dictionary=None,
++                             subject='alpha2 testing notification',):
      """
      Send notification email to settings.ADMINS.
  
      message = render_to_string(template_name, dictionary)
      sender = settings.SERVER_EMAIL
      try:
--        send_mail(_(ADMIN_NOTIFICATION_EMAIL_SUBJECT) % {'user': user.email},
++        send_mail(subject,
                    message, sender, [i[1] for i in settings.ADMINS])
      except (SMTPException, socket.error) as e:
          logger.exception(e)
          logger.log(LOGGING_LEVEL, msg)
  
  
++def send_account_creation_notification(template_name, dictionary=None):
++    user = dictionary.get('user', AstakosUser())
++    subject = _(ACCOUNT_CREATION_SUBJECT) % {'user': user.email}
++    return _send_admin_notification(template_name, dictionary, subject=subject)
++
++
++def send_group_creation_notification(template_name, dictionary=None):
++    group = dictionary.get('group', AstakosGroup())
++    subject = _(GROUP_CREATION_SUBJECT) % {'group': group.name}
++    return _send_admin_notification(template_name, dictionary, subject=subject)
++
++
  def send_helpdesk_notification(user, template_name='im/account_notification.txt'):
      """
      Send email to DEFAULT_CONTACT_EMAIL to notify for a new user activation.
      )
      sender = settings.SERVER_EMAIL
      try:
--        send_mail(_(HELPDESK_NOTIFICATION_EMAIL_SUBJECT) % {'user': user.email},
--                  message, sender, [DEFAULT_CONTACT_EMAIL])
++        send_mail(
++            _(HELPDESK_NOTIFICATION_EMAIL_SUBJECT) % {'user': user.email},
++            message, sender, [DEFAULT_CONTACT_EMAIL])
      except (SMTPException, socket.error) as e:
          logger.exception(e)
          raise SendNotificationError()
@@@ -189,6 -188,6 +199,8 @@@ def send_invitation(invitation, templat
      else:
          msg = 'Sent invitation %s' % invitation
          logger.log(LOGGING_LEVEL, msg)
++        invitation.inviter.invitations = max(0, self.invitations - 1)
++        invitation.inviter.save()
  
  
  def send_greeting(user, email_template_name='im/welcome_email.txt'):
@@@ -242,7 -241,7 +254,7 @@@ def send_change_email(ec, request, emai
          c = {'url': url, 'site_name': SITENAME}
          from_email = settings.SERVER_EMAIL
          send_mail(_(EMAIL_CHANGE_EMAIL_SUBJECT),
--            t.render(Context(c)), from_email, [ec.new_email_address])
++                  t.render(Context(c)), from_email, [ec.new_email_address])
      except (SMTPException, socket.error) as e:
          logger.exception(e)
          raise ChangeEmailError()
@@@ -266,35 -265,35 +278,21 @@@ def activate(user, email_template_name=
      send_greeting(user, email_template_name)
  
  
--def switch_account_to_shibboleth(user, local_user, greeting_template_name='im/welcome_email.txt'):
--    if not user or not isinstance(user, AstakosUser):
--        return
--
--    if not local_user or not isinstance(user, AstakosUser):
--        return
--
--    if not user.provider == 'shibboleth':
++def switch_account_to_shibboleth(user, local_user,
++                                 greeting_template_name='im/welcome_email.txt'):
++    try:
++        provider = user.provider
++    except AttributeError:
          return
--
--    user.delete()
--    local_user.provider = 'shibboleth'
--    local_user.third_party_identifier = user.third_party_identifier
--    local_user.save()
--    send_greeting(local_user, greeting_template_name)
--    return local_user
--
--
--def invite(invitation, inviter, email_template_name='im/welcome_email.txt'):
--    """
--    Send an invitation email and upon success reduces inviter's invitation by one.
--
--    Raises SendInvitationError
--    """
--    invitation.inviter = inviter
--    invitation.save()
--    send_invitation(invitation, email_template_name)
--    inviter.invitations = max(0, inviter.invitations - 1)
--    inviter.save()
++    else:
++        if not provider == 'shibboleth':
++            return
++        user.delete()
++        local_user.provider = 'shibboleth'
++        local_user.third_party_identifier = user.third_party_identifier
++        local_user.save()
++        send_greeting(local_user, greeting_template_name)
++        return local_user
  
  
  class SendMailError(Exception):
index 6f6b9b9,0000000..0cdeac8
mode 100644,000000..100644
--- /dev/null
@@@ -1,73 -1,0 +1,78 @@@
 +# 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.contrib.auth.models import Group, Permission
 +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"
-     
++
 +    def handle(self, *args, **options):
 +        if len(args) < 2:
-             raise CommandError("Please provide a group name and at least one permission")
-         
++            raise CommandError(
++                "Please provide a group name and at least one permission")
++
 +        group = None
 +        try:
 +            if args[0].isdigit():
 +                group = Group.objects.get(id=args[0])
 +            else:
 +                group = Group.objects.get(name=args[0])
 +        except Group.DoesNotExist, e:
 +            raise CommandError("Invalid group")
-         
++
 +        try:
 +            content_type = ContentType.objects.get(app_label='im',
-                                                        model='astakosuser')
++                                                   model='astakosuser')
 +            for pname in args[1:]:
 +                r, created = add_group_permission(group, pname)
 +                if created:
-                     self.stdout.write('Permission: %s created successfully\n' % pname)
++                    self.stdout.write(
++                        'Permission: %s created successfully\n' % pname)
 +                if r == 0:
-                     self.stdout.write('Group has already permission: %s\n' % pname)
++                    self.stdout.write(
++                        'Group has already permission: %s\n' % pname)
 +                else:
-                     self.stdout.write('Permission: %s added successfully\n' % pname)
++                    self.stdout.write(
++                        'Permission: %s added successfully\n' % pname)
 +        except Exception, e:
-             raise CommandError(e)
++            raise CommandError(e)
index 5ead6b0,0000000..581b383
mode 100644,000000..100644
--- /dev/null
@@@ -1,70 -1,0 +1,74 @@@
 +# 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.contrib.auth.models import Group
 +from django.core.exceptions import ValidationError
 +
 +from astakos.im.models import AstakosUser
 +from ._common import remove_group_permission
 +
++
 +class Command(BaseCommand):
 +    args = "<groupname> <permission> [<permissions> ...]"
 +    help = "Remove group permissions"
-     
++
 +    def handle(self, *args, **options):
 +        if len(args) < 2:
-             raise CommandError("Please provide a group name and at least one permission")
-         
++            raise CommandError(
++                "Please provide a group name and at least one permission")
++
 +        group = None
 +        try:
 +            if args[0].isdigit():
 +                group = Group.objects.get(id=args[0])
 +            else:
 +                group = Group.objects.get(name=args[0])
 +        except Group.DoesNotExist, e:
 +            raise CommandError("Invalid group")
-         
++
 +        try:
 +            for pname in args[1:]:
 +                r = remove_group_permission(group, pname)
 +                if r < 0:
-                     self.stdout.write('Invalid permission codename: %s\n' % pname)
++                    self.stdout.write(
++                        'Invalid permission codename: %s\n' % pname)
 +                elif r == 0:
 +                    self.stdout.write('Group has not permission: %s\n' % pname)
 +                elif r > 0:
-                     self.stdout.write('Permission: %s removed successfully\n' % pname)
++                    self.stdout.write(
++                        'Permission: %s removed successfully\n' % pname)
 +        except Exception, e:
-             raise CommandError(e)
++            raise CommandError(e)
@@@ -37,6 -37,6 +37,9 @@@ from django.db.utils import IntegrityEr
  from astakos.im.models import AstakosUser, Resource
  from astakos.im.endpoints.quotaholder import register_users, register_resources
  
++import logging
++logger = logging.getLogger(__name__)
++
  
  class Command(BaseCommand):
      help = "Send user information and resource quota in the Quotaholder"
@@@ -44,6 -44,6 +47,7 @@@
      def handle(self, *args, **options):
          try:
              register_resources(Resource.objects.all())
--            register_users(AstakosUser.objects.all())
++            register_users(AstakosUser.objects.filter(disturbed_quota=True))
          except BaseException, e:
--            raise CommandError("Bootstrap failed.")
++            logger.exception(e)
++            raise CommandError("Syncing failed.")
@@@ -38,7 -38,7 +38,7 @@@ from astakos.im.models import Resource
  
  
  class Command(BaseCommand):
--    args = "<service> <resource> [<key>=<value>...]"
++    args = "<service> <resource> <desc> <unit>"
      help = "Add a resource"
  
      def handle(self, *args, **options):
@@@ -41,106 -41,106 +41,112 @@@ from django.core.management.base impor
  from django.core.validators import validate_email
  from django.core.exceptions import ValidationError
  
--from astakos.im.models import AstakosUser, AstakosGroup, Membership
--from astakos.im.util import reserved_email
++from astakos.im.models import AstakosUser
++from astakos.im.api.callpoint import AstakosDjangoDBCallpoint
  
  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)
++
++
  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',
++                    dest='is_active',
                      default=False,
                      help="Activate user"),
          make_option('--admin',
                      action='store_true',
--                    dest='admin',
++                    dest='is_superuser',
                      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")
++        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
--
--        #try:
--        #    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()
++        email = args[0].decode('utf8')
  
--        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)
--        user.renew_token()
++        try:
++            validate_email(email)
++        except ValidationError:
++            raise CommandError("Invalid email")
  
--        if options['active']:
--            user.is_active = True
--        if options['admin']:
--            user.is_superuser = 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 = AstakosDjangoDBCallpoint()
++            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 = AstakosGroup.objects.get(name=groupname)
--                    Membership(group=group,
--                               person=user, date_joined=datetime.now()).save()
--                    self.stdout.write(
--                        'Group: %s added successfully\n' % groupname)
--                except AstakosGroup.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)
++#         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 = AstakosGroup.objects.get(name=groupname)
++#                     Membership(group=group,
++#                                person=user, date_joined=datetime.now()).save()
++#                     self.stdout.write(
++#                         'Group: %s added successfully\n' % groupname)
++#                 except AstakosGroup.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)
@@@ -35,7 -35,7 +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.functions import SendMailError
  from astakos.im.models import Invitation
  
  from ._common import get_user
@@@ -61,9 -61,9 +61,7 @@@ class Command(BaseCommand)
              realname = args[2]
  
              try:
--                invitation = Invitation(
--                    username=email, realname=realname, inviter=inviter)
--                invite(invitation, inviter)
++                inviter.invite(email, realname)
                  self.stdout.write("Invitation sent to '%s'\n" % (email,))
              except SendMailError, e:
                  transaction.rollback()
  # or implied, of GRNET S.A.
  
  from optparse import make_option
--from datetime import datetime
  
  from django.core.management.base import BaseCommand, CommandError
++from django.contrib.auth.models import Group, Permission
++from django.contrib.contenttypes.models import ContentType
  from django.core.exceptions import ValidationError
--from django.db.utils import IntegrityError
  
--from astakos.im.models import (AstakosUser, AstakosGroup, Membership, Resource,
--                               AstakosUserQuota)
--from astakos.im.endpoints.aquarium.producer import report_user_credits_event
++from astakos.im.models import AstakosUser
  from ._common import remove_user_permission, add_user_permission
  
  
@@@ -48,7 -48,7 +46,7 @@@ class Command(BaseCommand)
      args = "<user ID>"
      help = "Modify a user's attributes"
  
--    option_list = list(BaseCommand.option_list) + [
++    option_list = BaseCommand.option_list + (
          make_option('--invitations',
                      dest='invitations',
                      metavar='NUM',
          make_option('--delete-permission',
                      dest='delete-permission',
                      help="Delete user permission"),
--        make_option('--refill-credits',
--                    action='store_true',
--                    dest='refill',
--                    default=False,
--                    help="Refill user credits"),
--    ]
--    resources = Resource.objects.select_related().all()
--    append = option_list.append
--    for r in resources:
--        append(make_option('--%s-set-quota' % r,
--                    dest='%s-set-quota' % r,
--                    metavar='QUANTITY',
--                    help="Set resource quota"))
--    
++    )
++
      def handle(self, *args, **options):
          if len(args) != 1:
              raise CommandError("Please provide a user ID")
          groupname = options.get('add-group')
          if groupname is not None:
              try:
--                group = AstakosGroup.objects.get(name=groupname)
--                m = Membership(
--                    person=user, group=group, date_joined=datetime.now())
--                m.save()
--            except AstakosGroup.DoesNotExist, e:
++                group = Group.objects.get(name=groupname)
++                user.groups.add(group)
++            except Group.DoesNotExist, e:
                  self.stdout.write(
                      "Group named %s does not exist\n" % groupname)
--            except IntegrityError, e:
--                self.stdout.write("User is already member of %s\n" % groupname)
  
          groupname = options.get('delete-group')
          if groupname is not None:
              try:
--                group = AstakosGroup.objects.get(name=groupname)
--                m = Membership.objects.get(person=user, group=group)
--                m.delete()
--            except AstakosGroup.DoesNotExist, e:
++                group = Group.objects.get(name=groupname)
++                user.groups.remove(group)
++            except Group.DoesNotExist, e:
                  self.stdout.write(
                      "Group named %s does not exist\n" % groupname)
--            except Membership.DoesNotExist, e:
--                self.stdout.write("User is not a member of %s\n" % groupname)
  
          pname = options.get('add-permission')
          if pname is not None:
          if options['renew_token']:
              user.renew_token()
  
--        if options['refill']:
--            report_user_credits_event(user)
--
          try:
              user.save()
          except ValidationError, e:
  
          if password:
              self.stdout.write('User\'s new password: %s\n' % password)
--        
--        for r in self.resources:
--            limit = options.get('%s-set-quota' % r)
--            if not limit:
--                continue
--            if not limit.isdigit():
--                raise CommandError('Invalid limit')
--            
--            q = AstakosUserQuota.objects
--            q, created = q.get_or_create(resource=r, user=user,
--                                         defaults={'uplimit': limit})
--            verb = 'set' if created else 'updated'
--            self.stdout.write('User\'s quota %s successfully\n' % verb)
@@@ -2,16 -2,16 +2,17 @@@
  
  from south.v2 import DataMigration
  
++
  class Migration(DataMigration):
  
      def forwards(self, orm):
          "Obsolete migration."
          return
--    
++
      def backwards(self, orm):
          "Obsolete migration."
          return
--    
++
      models = {
          'auth.group': {
              'Meta': {'object_name': 'Group'},
@@@ -4,19 -4,19 +4,20 @@@ from south.db import d
  from south.v2 import SchemaMigration
  from django.db import models
  
++
  class Migration(SchemaMigration):
  
      def forwards(self, orm):
--        
--        # Changing field 'AstakosGroup.homepage'
--        db.alter_column('im_astakosgroup', 'homepage', self.gf('django.db.models.fields.URLField')(max_length=255, null=True))
  
++        # Changing field 'AstakosGroup.homepage'
++        db.alter_column('im_astakosgroup', 'homepage', self.gf(
++            'django.db.models.fields.URLField')(max_length=255, null=True))
  
      def backwards(self, orm):
--        
--        # Changing field 'AstakosGroup.homepage'
--        db.alter_column('im_astakosgroup', 'homepage', self.gf('django.db.models.fields.CharField')(max_length=255, null=True))
  
++        # Changing field 'AstakosGroup.homepage'
++        db.alter_column('im_astakosgroup', 'homepage', self.gf(
++            'django.db.models.fields.CharField')(max_length=255, null=True))
  
      models = {
          'auth.group': {
@@@ -4,26 -4,26 +4,25 @@@ from south.db import d
  from south.v2 import SchemaMigration
  from django.db import models
  
++
  class Migration(SchemaMigration):
  
      def forwards(self, orm):
--        
++
          # Adding field 'AstakosGroupQuota.uplimit'
          db.add_column('im_astakosgroupquota', 'uplimit', self.gf('django.db.models.fields.BigIntegerField')(null=True), keep_default=False)
  
          # Adding field 'AstakosUserQuota.uplimit'
          db.add_column('im_astakosuserquota', 'uplimit', self.gf('django.db.models.fields.BigIntegerField')(null=True), keep_default=False)
  
--
      def backwards(self, orm):
--        
++
          # Deleting field 'AstakosGroupQuota.uplimit'
          db.delete_column('im_astakosgroupquota', 'uplimit')
  
          # Deleting field 'AstakosUserQuota.uplimit'
          db.delete_column('im_astakosuserquota', 'uplimit')
  
--
      models = {
          'auth.group': {
              'Meta': {'object_name': 'Group'},
@@@ -4,13 -4,13 +4,14 @@@ from south.db import d
  from south.v2 import DataMigration
  from django.db import models
  
++
  class Migration(DataMigration):
  
      def forwards(self, orm):
          for q in orm.AstakosGroupQuota.objects.all():
              q.uplimit = q.limit
              q.save()
--    
++
      def backwards(self, orm):
          return
  
@@@ -17,13 -17,13 +17,13 @@@ class Migration(DataMigration)
  
          def create_policies(args):
              sn, dict = args
--            url = dict.get('url') 
++            url = dict.get('url')
              policy = dict.get('quota') or ()
              s, created = orm.Service.objects.get_or_create(name=sn,
                                                             defaults={'url': url})
              if not created and not s.url:
                  s.url = url
--                s.save()
++            s.save()
  
              for rn, l in policy.iteritems():
                  try:
@@@ -49,7 -49,7 +49,7 @@@
  
          def destroy_policies(args):
              sn, dict = args
--            url = dict.get('url') 
++            url = dict.get('url')
              policy = dict.get('quota') or ()
              for rn, l in policy.iteritems():
                  try:
@@@ -59,9 -59,9 +59,9 @@@
                      q.delete()
                  except orm.AstakosGroupQuota.DoesNotExist:
                      continue
--        
++
          map(destroy_policies, SERVICES.iteritems())
--    
++
      models = {
          'auth.group': {
              'Meta': {'object_name': 'Group'},
@@@ -4,25 -4,25 +4,28 @@@ from south.db import d
  from south.v2 import SchemaMigration
  from django.db import models
  
++
  class Migration(SchemaMigration):
  
      def forwards(self, orm):
--        
++
          # Changing field 'AstakosGroupQuota.limit'
--        db.alter_column('im_astakosgroupquota', 'limit', self.gf('django.db.models.fields.PositiveIntegerField')(null=True))
++        db.alter_column('im_astakosgroupquota', 'limit', self.gf(
++            'django.db.models.fields.PositiveIntegerField')(null=True))
  
          # Changing field 'AstakosUserQuota.limit'
--        db.alter_column('im_astakosuserquota', 'limit', self.gf('django.db.models.fields.PositiveIntegerField')(null=True))
--
++        db.alter_column('im_astakosuserquota', 'limit', self.gf(
++            'django.db.models.fields.PositiveIntegerField')(null=True))
  
      def backwards(self, orm):
--        
++
          # Changing field 'AstakosGroupQuota.limit'
--        db.alter_column('im_astakosgroupquota', 'limit', self.gf('django.db.models.fields.PositiveIntegerField')(default=None))
++        db.alter_column('im_astakosgroupquota', 'limit', self.gf(
++            'django.db.models.fields.PositiveIntegerField')(default=None))
  
          # Changing field 'AstakosUserQuota.limit'
--        db.alter_column('im_astakosuserquota', 'limit', self.gf('django.db.models.fields.PositiveIntegerField')(default=None))
--
++        db.alter_column('im_astakosuserquota', 'limit', self.gf(
++            'django.db.models.fields.PositiveIntegerField')(default=None))
  
      models = {
          'auth.group': {
index 2462101,0000000..9e530b6
mode 100644,000000..100644
--- /dev/null
@@@ -1,177 -1,0 +1,177 @@@
 +# encoding: utf-8
 +import datetime
 +from south.db import db
 +from south.v2 import DataMigration
 +from django.db import models
 +
++
 +class Migration(DataMigration):
 +
 +    def forwards(self, orm):
 +        "Write your forwards methods here."
 +        for u in orm.AstakosUser.objects.all():
 +            u.email = u.email.lower()
 +            u.save()
 +
 +    def backwards(self, orm):
 +        "Write your backwards methods here."
 +
 +    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, 10, 4, 9, 47, 13, 40029)', '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, 10, 4, 9, 47, 13, 34050)'}),
 +            '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'}),
 +            'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True', 'blank': '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', [], {'null': 'True'}),
 +            'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}),
 +            'uplimit': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'})
 +        },
 +        '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', [], {'null': 'True'}),
 +            'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}),
 +            'uplimit': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}),
 +            '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, 10, 4, 9, 47, 13, 41566)'}),
 +            '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, 10, 4, 9, 47, 13, 37772)', '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']
index 0000000,0000000..789da9e
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,186 @@@
++# encoding: utf-8
++import datetime
++from south.db import db
++from south.v2 import SchemaMigration
++from django.db import models
++
++
++class Migration(SchemaMigration):
++
++    def forwards(self, orm):
++
++        # Adding field 'Resource.desc'
++        db.add_column('im_resource', 'desc', self.gf('django.db.models.fields.TextField')(null=True), keep_default=False)
++
++        # Adding field 'Resource.unit'
++        db.add_column('im_resource', 'unit', self.gf('django.db.models.fields.CharField')(max_length=255, null=True), keep_default=False)
++
++    def backwards(self, orm):
++
++        # Deleting field 'Resource.desc'
++        db.delete_column('im_resource', 'desc')
++
++        # Deleting field 'Resource.unit'
++        db.delete_column('im_resource', 'unit')
++
++    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, 10, 30, 16, 37, 5, 608037)', '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, 10, 30, 16, 37, 5, 601308)'}),
++            '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'}),
++            'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True', 'blank': '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', [], {'null': 'True'}),
++            'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}),
++            'uplimit': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'})
++        },
++        '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', [], {'null': 'True'}),
++            'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}),
++            'uplimit': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}),
++            '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, 10, 30, 16, 37, 5, 609676)'}),
++            '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, 10, 30, 16, 37, 5, 605488)', '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'},
++            'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}),
++            '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']"}),
++            'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'})
++        },
++        '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']
index 0000000,0000000..ae47b5e
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,182 @@@
++# encoding: utf-8
++import datetime
++from south.db import db
++from south.v2 import SchemaMigration
++from django.db import models
++
++class Migration(SchemaMigration):
++
++    def forwards(self, orm):
++        
++        # Adding field 'AstakosUser.dirsturbed_quota'
++        db.add_column('im_astakosuser', 'dirsturbed_quota', self.gf('django.db.models.fields.BooleanField')(default=False, db_index=True), keep_default=False)
++
++
++    def backwards(self, orm):
++        
++        # Deleting field 'AstakosUser.dirsturbed_quota'
++        db.delete_column('im_astakosuser', 'dirsturbed_quota')
++
++
++    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, 11, 2, 11, 49, 56, 479805)', '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, 11, 2, 11, 49, 56, 473295)'}),
++            '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'}),
++            'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True', 'blank': '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', [], {'null': 'True'}),
++            'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}),
++            'uplimit': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'})
++        },
++        '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'}),
++            'dirsturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': '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', [], {'null': 'True'}),
++            'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}),
++            'uplimit': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}),
++            '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, 11, 2, 11, 49, 56, 481329)'}),
++            '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, 11, 2, 11, 49, 56, 477491)', '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'},
++            'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}),
++            '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']"}),
++            'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'})
++        },
++        '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']
index 0000000,0000000..239b3c5
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,183 @@@
++# encoding: utf-8
++import datetime
++from south.db import db
++from south.v2 import SchemaMigration
++from django.db import models
++
++class Migration(SchemaMigration):
++
++    def forwards(self, orm):
++        
++        # Adding field 'Resource.group'
++        db.add_column('im_resource', 'group', self.gf('django.db.models.fields.CharField')(max_length=255, null=True), keep_default=False)
++
++
++    def backwards(self, orm):
++        
++        # Deleting field 'Resource.group'
++        db.delete_column('im_resource', 'group')
++
++
++    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, 11, 2, 11, 50, 15, 262158)', '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, 11, 2, 11, 50, 15, 255876)'}),
++            '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'}),
++            'homepage': ('django.db.models.fields.URLField', [], {'max_length': '255', 'null': 'True', 'blank': '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', [], {'null': 'True'}),
++            'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}),
++            'uplimit': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'})
++        },
++        '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'}),
++            'dirsturbed_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': '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', [], {'null': 'True'}),
++            'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}),
++            'uplimit': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}),
++            '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, 11, 2, 11, 50, 15, 263696)'}),
++            '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, 11, 2, 11, 50, 15, 259896)', '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'},
++            'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}),
++            'group': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
++            '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']"}),
++            'unit': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'})
++        },
++        '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']
@@@ -41,13 -41,12 +41,15 @@@ from base64 import b64encod
  from random import randint
  from collections import defaultdict
  
--from django.db import models
--from django.contrib.auth.models import User, UserManager, Group
++from django.db import models, IntegrityError
++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.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
  
@@@ -57,11 -56,11 +59,20 @@@ from astakos.im.settings import (DEFAUL
  from astakos.im.endpoints.quotaholder 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
  
  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 = '.'
++
  
  class Service(models.Model):
      name = models.CharField('Name', max_length=255, unique=True, db_index=True)
@@@ -76,7 -75,7 +87,6 @@@
      def save(self, **kwargs):
          if not self.id:
              self.renew_token()
--        self.full_clean()
          super(Service, self).save(**kwargs)
  
      def renew_token(self):
      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)
@@@ -103,9 -102,9 +133,12 @@@ 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' % (self.service, self.name)
++        return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
  
  
  class GroupKind(models.Model):
@@@ -185,7 -184,7 +218,7 @@@ class AstakosGroup(Group)
      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()
          for q in self.astakosgroupquota_set.select_related().all():
              d[q.resource] += q.uplimit
          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 = policies.get('service', None)
++            resource = policies.get('resource', None)
++            uplimit = policies.get('uplimit', 0)
++            update = policies.get('update', True)
++            self.add_policy(service, resource, uplimit, update)
++    
      @property
      def owners(self):
          return self.owner.all()
@@@ -245,7 -244,7 +305,7 @@@ class AstakosUser(User)
  
      has_credits = models.BooleanField('Has credits?', default=False)
      has_signed_terms = models.BooleanField(
--        'Agree with the terms?', default=False)
++        'I agree with the terms', default=False)
      date_signed_terms = models.DateTimeField(
          'Signed terms date', null=True, blank=True)
  
          through='Membership')
  
      __has_signed_terms = False
++    dirsturbed_quota = models.BooleanField('Needs quotaholder syncing',
++                                           default=False, db_index=True)
  
      owner = models.ManyToManyField(
          AstakosGroup, related_name='owner', null=True)
      def __init__(self, *args, **kwargs):
          super(AstakosUser, self).__init__(*args, **kwargs)
          self.__has_signed_terms = self.has_signed_terms
--        if not self.id:
++        if not self.id and not self.is_active:
              self.is_active = False
  
      @property
          else:
              self.last_name = parts[0]
  
++    def add_permission(self, pname):
++        if self.has_perm(pname):
++            return
++        p, created = Permission.objects.get_or_create(codename=pname,
++                                                      name=pname.capitalize(),
++                                                      content_type=content_type)
++        self.user_permissions.add(p)
++
++    def remove_permission(self, pname):
++        if self.has_perm(pname):
++            return
++        p = Permission.objects.get(codename=pname,
++                                   content_type=content_type)
++        self.user_permissions.remove(p)
++
      @property
      def invitation(self):
          try:
          except Invitation.DoesNotExist:
              return None
  
++    def invite(self, email, realname):
++        inv = Invitation(inviter=self, username=email, realname=realname)
++        inv.save()
++        send_invitation(inv)
++        self.invitations = max(0, self.invitations - 1)
++        self.save()
++
      @property
      def quota(self):
++        """Returns a dict with the sum of quota limits per resource"""
          d = defaultdict(int)
--        for q in self.astakosuserquota_set.select_related().all():
++        for q in self.policies:
              d[q.resource] += q.uplimit
--        for m in self.membership_set.select_related().all():
++        for m in self.extended_groups:
              if not m.is_approved:
                  continue
              g = m.group
                  continue
              for r, uplimit in g.quota.iteritems():
                  d[r] += uplimit
--        
++
          # TODO set default for remaining
          return d
  
++    @property
++    def policies(self):
++        return self.astakosuserquota_set.select_related().all()
++
++    @policies.setter
++    def policies(self, policies):
++        for p in policies:
++            service = policies.get('service', None)
++            resource = policies.get('resource', None)
++            uplimit = policies.get('uplimit', 0)
++            update = policies.get('update', True)
++            self.add_policy(service, resource, uplimit, update)
++
++    def add_policy(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)
++
++    def remove_policy(self, service, resource):
++        """Raises ObjectDoesNotExist, IntegrityError"""
++        resource = Resource.objects.get(service__name=service, name=resource)
++        q = self.policies.get(resource=resource).delete()
++
++    @property
++    def extended_groups(self):
++        return self.membership_set.select_related().all()
++
++    @extended_groups.setter
++    def extended_groups(self, groups):
++        #TODO exceptions
++        for name in groups:
++            group = AstakosGroup.objects.get(name=name)
++            self.membership_set.create(group=group)
++
      def save(self, update_timestamps=True, **kwargs):
          if update_timestamps:
              if not self.id:
              return False
          return True
  
++    def store_disturbed_quota(self, set=True):
++        self.disturbed_quota = set
++        self.save()
++
  
  class Membership(models.Model):
      person = models.ForeignKey(AstakosUser)
          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)
      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)
@@@ -522,8 -518,7 +680,8 @@@ class EmailChangeManager(models.Manager
  
  
  class EmailChange(models.Model):
 -    new_email_address = models.EmailField(_(u'new e-mail address'), help_text=_(u'Your old email address will be used until you verify your new one.'))
 +    new_email_address = models.EmailField(_(u'new e-mail address'),
-         help_text=_(u'Your old email address will be used until you verify your new one.'))
++                                          help_text=_(u'Your old email address will be used until you verify your new one.'))
      user = models.ForeignKey(
          AstakosUser, unique=True, related_name='emailchange_user')
      requested_at = models.DateTimeField(default=datetime.now())
@@@ -611,8 -606,8 +769,7 @@@ def astakosuser_pre_save(sender, instan
      else:
          get = AstakosUser.__getattribute__
          l = filter(lambda f: get(db_instance, f) != get(instance, f),
--                   BILLING_FIELDS
--                   )
++                   BILLING_FIELDS)
          instance.aquarium_report = True if l else False
  
  
@@@ -624,6 -619,6 +781,7 @@@ def astakosuser_post_save(sender, insta
      set_default_group(instance)
      # TODO handle socket.error & IOError
      register_users((instance,))
++    instance.renew_token()
  
  
  def resource_post_save(sender, instance, created, **kwargs):
@@@ -648,14 -643,14 +806,13 @@@ def send_quota_disturbed(sender, instan
      elif sender == AstakosGroup:
          if not instance.is_enabled:
              return
--    quota_disturbed.send(sender=sender, users=users)
++    map(lambda u: u.store_disturbed_quota, users)
  
--
--def on_quota_disturbed(sender, users, **kwargs):
--    print '>>>', locals()
--    if not users:
--        return
--    send_quota(users)
++# def on_quota_disturbed(sender, users, **kwargs):
++#     print '>>>', locals()
++#     if not users:
++#         return
++#     send_quota(users)
  
  post_syncdb.connect(fix_superusers)
  post_save.connect(user_post_save, sender=User)
@@@ -664,7 -659,7 +821,7 @@@ post_save.connect(astakosuser_post_save
  post_save.connect(resource_post_save, sender=Resource)
  
  quota_disturbed = Signal(providing_args=["users"])
--quota_disturbed.connect(on_quota_disturbed)
++# quota_disturbed.connect(on_quota_disturbed)
  
  post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
  post_delete.connect(send_quota_disturbed, sender=Membership)
@@@ -103,38 -103,53 +103,47 @@@ 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)
 -
 -# Configurable email subjects
 -INVITATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_INVITATION_EMAIL_SUBJECT',
 -        'Invitation to %s alpha2 testing' % SITENAME)
 -GREETING_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_GREETING_EMAIL_SUBJECT',
 -        'Welcome to %s alpha2 testing' % SITENAME)
 -FEEDBACK_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_FEEDBACK_EMAIL_SUBJECT',
 -        'Feedback from %s alpha2 testing' % SITENAME)
 -VERIFICATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_VERIFICATION_EMAIL_SUBJECT',
 -        '%s alpha2 testing account activation is needed' % SITENAME)
 -ADMIN_NOTIFICATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_ADMIN_NOTIFICATION_EMAIL_SUBJECT',
 -        '%s alpha2 testing account created (%%(user)s)' % SITENAME)
 -HELPDESK_NOTIFICATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_HELPDESK_NOTIFICATION_EMAIL_SUBJECT',
 -        '%s alpha2 testing account activated (%%(user)s)' % SITENAME)
 -EMAIL_CHANGE_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_EMAIL_CHANGE_EMAIL_SUBJECT',
 -        'Email change on %s alpha2 testing' % SITENAME)
 -PASSWORD_RESET_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT',
 -        'Password reset on %s alpha2 testing' % SITENAME)
++                                 '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/',
++                   {'cyclades': {'url': 'https://node1.example.com/ui/',
                                   'quota': {'vm': 2}},
--                    'pithos+':  {'url':'https://node2.example.com/ui/',
--                                 'quota': {'diskspace': 50 * 1024 * 1024 * 1024}}})
++                    'pithos+': {'url': 'https://node2.example.com/ui/',
++                                'quota': {'diskspace': 50 * 1024 * 1024 * 1024}}})
  
  # Set the billing URI
  AQUARIUM_URL = getattr(settings, 'ASTAKOS_AQUARIUM_URL', '')
  
  # Set how many objects should be displayed per page
 -PAGINATE_BY = getattr(settings, 'ASTAKOS_PAGINATE_BY', 8)
 +PAGINATE_BY = getattr(settings, 'ASTAKOS_PAGINATE_BY', 8)
 +
 +# Enforce token renewal on password change/reset
- NEWPASSWD_INVALIDATE_TOKEN = getattr(settings, 'ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN', True)
++NEWPASSWD_INVALIDATE_TOKEN = getattr(
++    settings, 'ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN', True)
@@@ -350,5 -350,66 +350,62 @@@ table.alt-style tr td a.more-info:hove
  table.alt-style tr td a.open                                  { background-position:-16px 0} \r
  \r
  .projects .details a.edit                                             { float:right; margin-left:20px;  }\r
- .projects .details .data                                              { overflow:hidden; }\r
- .projects .editable form textarea                             { width:70%; height:50px; max-width:70%; width:270px; height:120px;}
+ .projects .details .data                                              { overflow:hidden; }
+ .projects .editable form textarea                             { width:70%; height:50px; max-width:70%; width:270px; height:120px;}\r
\r
\r
+ /* quotas-form  */\r
\r
+ .quotas-form fieldset                                                 { background:url(../images/dots.jpg) repeat-x scroll center bottom transparent; margin-bottom:3em; padding-bottom:3em; position:relative; }\r
 -.quotas-form fieldset legend                                  { color:#55B577; font-size:1.154em; margin-bottom:3em; position:relative; }\r
+ .quotas-form fieldset legend span                             { color:#222; }\r
+ .quotas-form .with-checkbox .checkbox-widget  { margin-top:12px; } \r
+ .quotas-form .with-checkbox span.info                         { bottom:22px; }\r
 -.quotas-form .form-row.submit                                 { text-align:center; }\r
+ .quotas-form input[type="submit"]                             { margin:15px 0; background-color:#B3B3B3 }\r
+ .quotas-form input[type="submit"]:hover                       { background:#55B577 }\r
+ .quotas-form input[type="submit"]:focus                       { border-color: #B3B3B3}\r
+ .quotas-form input[type="submit"]:focus:hover { border-color: #55B577}\r
+ .quotas-form fieldset ul                                              { padding:0; margin:0 0 1em; }\r
+ .quotas-form fieldset ul li                                           { list-style:none outside none; float:left; padding:0 0 0 60px; margin:0; }\r
+ .quotas-form fieldset ul li:first-child                       { padding-left:0; }\r
+ .quotas-form fieldset ul li a                                 { display:block; width:82px; height:82px; overflow:hidden; }\r
+ .quotas-form fieldset ul li a:hover   img                     { margin-top:-151px; }\r
+ .quotas-form fieldset ul li a.selected img            { margin-top:-313px; }\r
 -.quotas-form fieldset ul li a.selected:focus  { outline:0 none; }\r
+ .quotas-form p.msg                                                            { color:#B3B3B3; }\r
+ .quotas-form a.delete                                                 { position:absolute; right:0; top:0; color:#B3B3B3; z-index:2 }\r
+ .quotas-form .group                                                           { display:none; position:relative; background:url(../images/dots.jpg) repeat-x scroll center bottom; margin-bottom:2em; padding-bottom:2em;}\r
+ .quotas-form .group fieldset                                  { background:transparent; margin-bottom:1em; padding-bottom:1em; }\r
+ .quotas-form .group fieldset legend                           { margin-bottom:1em; padding-bottom:1em; }\r
+ .quotas-form fieldset ul li.rel+li.rel                        { background:url(../images/quota-related-bg.png) no-repeat left center; }\r
+ .quotas-form .double-checks label                             { font-size:1.077em; }\r
+ .quotas-form .double-checks .form-row                 { float:left; margin-right:10px;}\r
+ .quotas-form .double-checks .with-checkbox .checkbox-widget   { left:0; }\r
+ .quotas-form .double-checks .with-checkbox input[type="text"] { width:60px; float:left; margin:9px 15px -9px; display:none; padding:6px; }\r
+ .quotas-form .double-checks .with-checkbox label{ width:auto; float:left; margin-left:35px; }\r
+ .quotas-form .double-checks .with-checkbox input[type="text"].hideshow        { display:block; }\r
+ form.quotas-form legend span.info                             { top:0; bottom:auto; left:250px; }\r
+ form.quotas-form legend span.info span                        { width:400px; }\r
+ .quotas-form .with-checkbox+.with-checkbox            { width:196px; }\r
+  \r
 -/* stats */\r
+ .stats ul                                                                             { margin:0; padding:0; list-style:none outside none; }\r
+ .stats ul li                                                                  { margin:0 0 1em 0; padding:0 0 1em 0; list-style:none outside none; background:url(../images/stats-line.jpg) repeat-x left bottom}\r
+ .stats .bar                                                                           { padding:20px 0; text-align:center;  float:left; width:200px;}\r
+ .stats .bar div                                                                       { width:340px; height:30px; border:1px solid #000;}\r
+ .stats .bar span                                                              { text-align:right; display:block; height:100%; color:#fff;  line-height:30px; font-size:1.231em; text-indent:50px;}\r
+ .stats .red .bar span                                                 { background:#ef4f54; }\r
+ .stats .yellow .bar span                                              { background:#f6921e; }\r
+ .stats .green .bar span                                                       { background:#55b577; }\r
+ .stats .img-wrap                                                              { float:left; width:100px; background:url(../images/statistics_icons.png) no-repeat center center; padding:30px 0; }\r
+ .stats .info                                                                  { margin:0 25px ; width:320px; float:left;  }\r
+ .stats .info p                                                                        { color:#999; margin:0; }\r
+ .stats .info h3                                                                       { font-size:1.231em; color:#222222 }\r
+ .stats .vm .img-wrap                                                  { background-image:url(../images/vm-stats.png) }\r
+ .stats .ram .img-wrap                                                 { background-image:url(../images/ram-stats.png) }\r
+ .stats .network .img-wrap                                             { background-image:url(../images/network-stats.png) }\r
+ .stats .disk .img-wrap                                                        { background-image:url(../images/disk-stats.png) }\r
+ .stats .storage .img-wrap                                             { background-image:url(../images/storage-stats.png) }\r
+ .stats .bandwidth .img-wrap                                           { background-image:url(../images/bandwidth-stats.png) }\r
\r
+ .stats .red .img-wrap                                                 { background-position: 15px 7px; }\r
+ .stats .yellow .img-wrap                                              { background-position: -124px 7px; }\r
+ .stats .green .img-wrap                                                       { background-position: -263px 7px; }\r
+ .projects .editable form textarea                             { width:70%; height:50px; max-width:70%; width:270px; height:120px;}
@@@ -43,9 -43,9 +43,9 @@@ installed_apps = 
      'django.contrib.contenttypes',
      'django.contrib.sessions',
      'django.contrib.messages',
 -#    'djcelery',
 -#    'debug_toolbar',
 +    'djcelery',
- #     'debug_toolbar'
-     ]
++    'debug_toolbar',
+ ]
  
  context_processors = [
      'django.core.context_processors.media',
@@@ -68,7 -68,7 +68,7 @@@ middlware_classes = 
      'synnefo.lib.middleware.LoggingConfigMiddleware',
      'synnefo.lib.middleware.SecureMiddleware',
      'django.middleware.csrf.CsrfViewMiddleware',
- #     'debug_toolbar.middleware.DebugToolbarMiddleware'
 -#    'debug_toolbar.middleware.DebugToolbarMiddleware',
++    'debug_toolbar.middleware.DebugToolbarMiddleware',
  ]
  
  loggers = {
@@@ -94,4 -94,4 +94,4 @@@ djcelery.setup_loader(
  
  BROKER_URL = ''
  
--INTERNAL_IPS = ('127.0.0.1',)
++INTERNAL_IPS = ('127.0.0.1',)
@@@ -111,5 -112,8 +111,5 @@@ def login(request, backend=None, on_log
          return render_response(on_creation_template,
                                 signup_form=form,
                                 provider='shibboleth',
 -                               context_instance=get_context(
 -                               request,
 -                               extra_context
 -                               )
 -                               )
 +                               context_instance=get_context(request,
-                                                             extra_context))
++                                                            extra_context))
@@@ -4,7 -4,69 +4,66 @@@
  
  {% block page.body %}
  <div class="maincol {% block innerpage.class %}{% endblock %}">
+     <div class="stats clearfix">
+               <ul>
+                       {% for r in data.resources %}
+                        
+               <li class="clearfix  {{ r.load_class }} {{ r.name}}">
+                       <div class="img-wrap">&nbsp;</div>
+                       <div class="info">
+                               <h3>{{ r.description }}</h3>
+                               <p>
+                                       {{ r.ratio|floatformat }}% Used<br>
+                                       You are using {{ r.currValue }} {{ r.unit }} out of your {{ r.maxValue }}{{ r.unit }} 
+                                       {% if r.maxValue == '1' %}
+                                               {{ r.name|capfirst}}
+                                       {% else %} 
+                                               {{ r.plural|capfirst }}
+                                       {% endif %}
+                                        - Aouch!
+                               </p>
+                       </div>
+                       <div class="bar">
+                               <div><span style="width:{{ r.ratio|floatformat }}%;">{{ r.ratio|floatformat }}% &nbsp;&nbsp;</span></div>
+                       </div>
+               </li>
+               {% endfor %}
+       </ul>
+     </div>    
+     <!--
      <div class="section">
+             {% for k, v in user.quota|items %}
+                 <strong>{{k}}</strong>
+                 <table class="zebra-striped id-sorted">
 -<!-- 
+                     <thead>
+                       <tr>
+                         <th>Limit (Group)</th>
+                       </tr>
+                     </thead>
 - -->
 - <!--
+                     <tbody>
+                     {% for m in user.membership_set.select_related.all %}
+                         {% if m.group.is_enabled %}
+                             {% with m.group.quota as quota %}
+                             {% if  quota %}
+                                 {% for kk, vv in quota|items %}
+                                     {% if k == kk %}
+                                     <tr>
+                                         <td>{{ vv }} ({{m.group.name}})</td>
+                                     </tr>
+                                     {% endif %}
+                                 {% endfor %}
+                             {% endif %}
+                             {% endwith %}
+                         {% endif %}
+                     {% endfor %}
+                                     <tr>
+                                         <td><strong>{{ v }}</strong></td>
+                                     </tr>
+                                     <tr/>
+                     </tbody>
+                 </table>
+             {% endfor %}
+     </div>
          <form action="" method="post"
                    class="withlabels">{% csrf_token %}
                    {% include "im/form_render.html" %}
index 0000000,0000000..4feec73
new file mode 100644 (file)
--- /dev/null
--- /dev/null
@@@ -1,0 -1,0 +1,28 @@@
++{% extends "im/account_base.html" %}
++
++{% load filters %}
++
++{% block page.body %}
++<div class="maincol {% block innerpage.class %}{% endblock %}">
++    <div class="stats clearfix">
++              <ul>
++                      {% for r in data %}
++                       
++              <li class="clearfix  {{ r.load_class }} {{ r.name}}">
++                      <div class="img-wrap">&nbsp;</div>
++                      <div class="info">
++                              <h3>{{ r.description }}</h3>
++                              <p>
++                                      {{ r.ratio|floatformat }}% Used<br>
++                                      You are using {{ r.currValue }} {{ r.unit }} out of your {{ r.maxValue }}{{ r.unit }} {{ r.plural|capfirst }} - Aouch!
++                              </p>
++                      </div>
++                      <div class="bar">
++                              <div><span style="width:{{ r.ratio|floatformat }}%;">{{ r.ratio|floatformat }}% &nbsp;&nbsp;</span></div>
++                      </div>
++              </li>
++              {% endfor %}
++      </ul>
++    </div>    
++</div>
++{% endblock %}
@@@ -15,10 -15,10 +15,12 @@@ MESSAGES_VIEWS_MAP = getattr(settings, 
      'astakos.im.views.feedback': 'PROFILE_MESSAGES',
  })
  
++
  @register.tag(name='display_messages')
  def display_messages(parser, token):
      return MessagesNode()
  
++
  class DummyMessage(object):
      def __init__(self, type, msg):
          self.message = msg
@@@ -27,6 -27,6 +29,7 @@@
      def __repr__(self):
          return "%s: %s" % (self.tags, self.message)
  
++
  class MessagesNode(template.Node):
  
      def get_view_messages(self, context):
@@@ -53,7 -53,7 +56,8 @@@
              cls = messages[-1].tags
              content = '<div class="top-msg active %s">' % cls
              for msg in messages:
--                content += '<div class="msg %s">%s</div>' % (msg.tags, msg.message)
++                content += '<div class="msg %s">%s</div>' % (
++                    msg.tags, msg.message)
  
              content += '<a href="#" title="close" class="close">X</a>'
              content += '</div>'
@@@ -46,31 -46,31 +46,34 @@@ 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)
++    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() )
--    
++    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() )
--        
++        timestamp = calendar.timegm(date.utctimetuple())
++
      return months
--    
++
++
  @register.filter
  def lookup(d, key):
      return d.get(key)
@@@ -84,18 -84,18 +87,19 @@@ def dkeys(d)
  @register.filter
  def month_name(month_number):
      return calendar.month_name[month_number]
--    
++
  
  @register.filter
--def todate(value, arg = ''):
++def todate(value, arg=''):
      secs = int(value) / 1000
      return datetime.datetime.fromtimestamp(secs)
  
  
  @register.filter
--def rcut(value, chars = '/'):
++def rcut(value, chars='/'):
      return value.rstrip(chars)
  
++
  @register.filter
  def paginate(l, args):
      page, delim, sorting = args.partition(DELIM)
              default = ''
              if sorting.endswith('_date'):
                  default = datetime.datetime.utcfromtimestamp(0)
--            l.sort(key=lambda i: getattr(i, sorting) \
--                        if getattr(i, sorting) else default)
--            
++            l.sort(key=lambda i: getattr(i, sorting)
++                   if getattr(i, sorting) else default)
++
      paginator = Paginator(l, PAGINATE_BY)
--    
++
      try:
          page_number = int(page)
      except ValueError:
          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
++    return d
@@@ -77,9 -75,9 +77,11 @@@ urlpatterns = patterns('astakos.im.view
                             {}, 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/?$',
++                       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/?$',
++                       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'),
@@@ -103,16 -103,20 +107,20 @@@ if 'local' in IM_MODULES
                              url(r'^local/?$', 'local.login')
                              )
      urlpatterns += patterns('django.contrib.auth.views',
-         url(r'^local/password_reset/?$', 'password_reset',
-          {'email_template_name':'registration/password_email.txt',
-           'password_reset_form':ExtendedPasswordResetForm}),
-         url(r'^local/password_reset_done/?$', 'password_reset_done'),
-         url(r'^local/reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/?$',
-          'password_reset_confirm', {'set_password_form':ExtendedSetPasswordForm}),
-         url(r'^local/password/reset/complete/?$', 'password_reset_complete'),
-         url(r'^password_change/?$', 'password_change', {'post_change_redirect':'profile',
-             'password_change_form':ExtendedPasswordChangeForm})
-     )
+                             url(r'^local/password_reset/?$', 'password_reset',
+                                 {'email_template_name': 'registration/password_email.txt',
+                                  'password_reset_form': ExtendedPasswordResetForm}),
+                             url(r'^local/password_reset_done/?$',
+                                 'password_reset_done'),
+                             url(
+                                 r'^local/reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/?$',
 -                            'password_reset_confirm'),
++                            'password_reset_confirm', {'set_password_form': ExtendedSetPasswordForm}),
+                             url(r'^local/password/reset/complete/?$',
+                                 'password_reset_complete'),
+                             url(
+                             r'^password_change/?$', 'password_change', {'post_change_redirect': 'profile',
+                                                                         'password_change_form': ExtendedPasswordChangeForm})
+                             )
  
  if INVITATIONS_ENABLED:
      urlpatterns += patterns('astakos.im.views',
@@@ -43,8 -43,8 +43,8 @@@ from django.template import RequestCont
  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.core.exceptions import ValidationError, ObjectDoesNotExist
++from django.db.models.fields import Field
  from astakos.im.models import AstakosUser, Invitation
  from astakos.im.settings import COOKIE_NAME, \
      COOKIE_DOMAIN, COOKIE_SECURE, FORCE_PROFILE_UPDATE, LOGGING_LEVEL
@@@ -178,4 -179,4 +178,54 @@@ def get_query(request)
      try:
          return request.__getattribute__(request.method)
      except AttributeError:
-         return {}
 -        return request.GET
++        return {}
++
++
++def model_to_dict(obj, exclude=['AutoField', 'ForeignKey', 'OneToOneField'],
++                  include_empty=True):
++    '''
++        serialize model object to dict with related objects
++
++        author: Vadym Zakovinko <vp@zakovinko.com>
++        date: January 31, 2011
++        http://djangosnippets.org/snippets/2342/
++    '''
++    tree = {}
++    for field_name in obj._meta.get_all_field_names():
++        try:
++            field = getattr(obj, field_name)
++        except (ObjectDoesNotExist, AttributeError):
++            continue
++
++        if field.__class__.__name__ in ['RelatedManager', 'ManyRelatedManager']:
++            if field.model.__name__ in exclude:
++                continue
++
++            if field.__class__.__name__ == 'ManyRelatedManager':
++                exclude.append(obj.__class__.__name__)
++            subtree = []
++            for related_obj in getattr(obj, field_name).all():
++                value = model_to_dict(related_obj, exclude=exclude)
++                if value or include_empty:
++                    subtree.append(value)
++            if subtree or include_empty:
++                tree[field_name] = subtree
++            continue
++
++        field = obj._meta.get_field_by_name(field_name)[0]
++        if field.__class__.__name__ in exclude:
++            continue
++
++        if field.__class__.__name__ == 'RelatedObject':
++            exclude.append(field.model.__name__)
++            tree[field_name] = model_to_dict(getattr(obj, field_name),
++                                             exclude=exclude)
++            continue
++
++        value = getattr(obj, field_name)
++        if field.__class__.__name__ == 'ForeignKey':
++            value = unicode(value) if value is not None else value
++        if value or include_empty:
++            tree[field_name] = value
++
++    return tree
@@@ -33,6 -33,6 +33,9 @@@
  
  import logging
  import calendar
++import inflect
++
++engine = inflect.engine()
  
  from urllib import quote
  from functools import wraps
@@@ -45,11 -45,10 +48,11 @@@ from django.contrib.auth.views import p
  from django.core.urlresolvers import reverse
  from django.db import transaction
  from django.db.models import Q
- <<<<<<< HEAD
  from django.db.utils import IntegrityError
  from django.forms.fields import URLField
--from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, \
--    HttpResponseRedirect, HttpResponseBadRequest, Http404
++from django.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
@@@ -60,11 -59,9 +63,11 @@@ from django.views.generic.list_detail i
  from django.http import HttpResponseBadRequest
  from django.core.xheaders import populate_xheaders
  
--from astakos.im.models import (
--    AstakosUser, ApprovalTerms, AstakosGroup, Resource,
--    EmailChange, GroupKind, Membership, AstakosGroupQuota)
++from astakos.im.models import (AstakosUser, ApprovalTerms, AstakosGroup,
++                               Resource, EmailChange, GroupKind, Membership,
++                               AstakosGroupQuota)
 +from django.views.decorators.http import require_http_methods
 +
  from astakos.im.activation_backends import get_backend, SimpleBackend
  from astakos.im.util import get_context, prepare_response, set_cookie, get_query
  from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
                                AstakosGroupSortForm, MembersSortForm,
                                TimelineForm, PickResourceForm)
  from astakos.im.functions import (send_feedback, SendMailError,
--                                  invite as invite_func, logout as auth_logout,
++                                  logout as auth_logout,
                                    activate as activate_func,
                                    switch_account_to_shibboleth,
--                                  send_admin_notification,
++                                  send_group_creation_notification,
                                    SendNotificationError)
  from astakos.im.endpoints.quotaholder import timeline_charge
--from astakos.im.settings import (
--    COOKIE_NAME, COOKIE_DOMAIN, SITENAME, LOGOUT_NEXT,
--    LOGGING_LEVEL, PAGINATE_BY)
++from astakos.im.settings import (COOKIE_NAME, COOKIE_DOMAIN, LOGOUT_NEXT,
++                                 LOGGING_LEVEL, PAGINATE_BY)
  from astakos.im.tasks import request_billing
++from astakos.im.api.callpoint import AstakosDjangoDBCallpoint
  
  logger = logging.getLogger(__name__)
  
@@@ -92,6 -89,6 +95,7 @@@
  DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
                                       'https://', '')"""
  
++
  def render_response(template, tab=None, status=200, reset_cookie=False,
                      context_instance=None, **kwargs):
      """
@@@ -220,9 -215,9 +224,10 @@@ def invite(request, template_name='im/i
          if inviter.invitations > 0:
              if form.is_valid():
                  try:
--                    invitation = form.save()
--                    invite_func(invitation, inviter)
--                    message = _('Invitation sent to %s' % invitation.username)
++                    email = form.cleaned_data.get('username')
++                    realname = form.cleaned_data.get('realname')
++                    inviter.invite(email, realname)
++                    message = _('Invitation sent to %s' % email)
                      messages.success(request, message)
                  except SendMailError, e:
                      message = e.message
@@@ -475,7 -466,7 +480,8 @@@ def logout(request, template='registrat
          return response
      messages.success(request, _('You have successfully logged out.'))
      context = get_context(request, extra_context)
--    response.write(template_loader.render_to_string(template, context_instance=context))
++    response.write(
++        template_loader.render_to_string(template, context_instance=context))
      return response
  
  
@@@ -569,6 -558,6 +575,7 @@@ def approval_terms(request, term_id=Non
              pass
  
      if not term:
++        messages.error(request, 'There are no approval terms.')
          return HttpResponseRedirect(reverse('index'))
      f = open(term.location, 'r')
      terms = f.read()
@@@ -602,9 -590,9 +609,10 @@@ def change_password(request)
                             post_change_redirect=reverse('edit_profile'),
                             password_change_form=ExtendedPasswordChangeForm)
  
 -@signed_terms_required
 +@require_http_methods(["GET", "POST"])
  @login_required
 +@signed_terms_required
  @transaction.commit_manually
  def change_email(request, activation_key=None,
                   email_template_name='registration/email_change_email.txt',
@@@ -700,14 -688,14 +708,13 @@@ def group_add(request, kind_name='defau
  
              # send notification
              try:
--                send_admin_notification(
++                send_group_creation_notification(
                      template_name='im/group_creation_notification.txt',
                      dictionary={
                          'group': new_object,
                          'owner': request.user,
                          'policies': policies,
--                    },
--                    subject='%s alpha2 testing group creation notification' % SITENAME
++                    }
                  )
              except SendNotificationError, e:
                  messages.error(request, e, fail_silently=True)
@@@ -767,7 -755,7 +774,7 @@@ def group_list(request)
              d['own'].append(g)
          else:
              d['other'].append(g)
--    
++
      # validate sorting
      fields = ('own', 'other')
      for f in fields:
              if not form.is_valid():
                  globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
      return object_list(request, queryset=none,
--                       extra_context={'is_search':False,
++                       extra_context={'is_search': False,
                                        'mine': d['own'],
                                        'other': d['other'],
                                        'own_sorting': own_sorting,
@@@ -804,7 -792,7 +811,7 @@@ def group_detail(request, group_id)
                          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
      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():
++        for k, v in request.POST.iteritems():
              if k in update_form.fields:
                  update_data[k] = v
              if k in addmembers_form.fields:
          if addmembers_form.is_valid():
              map(obj.approve_member, addmembers_form.valid_users)
              addmembers_form = AddGroupMembersForm()
--    
--    template_name = "%s/%s_detail.html" % (model._meta.app_label, model._meta.object_name.lower())
++
++    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')
++    sorting = request.GET.get('sorting')
      if sorting:
          form = MembersSortForm({'sort_by': sorting})
          if form.is_valid():
              sorting = form.cleaned_data.get('sort_by')
--         
++
      extra_context = {'update_form': update_form,
                       'addmembers_form': addmembers_form,
                       'page': request.GET.get('page', 1),
          else:
              c[key] = value
      response = HttpResponse(t.render(c), mimetype=mimetype)
--    populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.name))
++    populate_xheaders(
++        request, response, model, getattr(obj, obj._meta.pk.name))
      return response
  
  
@@@ -877,23 -865,23 +886,23 @@@ def group_search(request, extra_context
          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': """
++                                  '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': """
++                                  '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': """
++                                  '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': """
++                                  'is_owner': """
                      SELECT CASE WHEN EXISTS(
                      SELECT id FROM im_astakosuser_owner
                      WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
                             q=q,
                             sorting=sorting))
  
++
  @signed_terms_required
  @login_required
  def group_all(request, extra_context=None, **kwargs):
          # TODO check sorting value
          q = q.order_by(sorting)
      return object_list(
--                request,
--                q,
--                paginate_by=PAGINATE_BY,
--                page=request.GET.get('page') or 1,
--                template_name='im/astakosgroup_list.html',
--                extra_context=dict(form=AstakosGroupSearchForm(),
--                                   is_search=True,
--                                   sorting=sorting))
++        request,
++        q,
++        paginate_by=PAGINATE_BY,
++        page=request.GET.get('page') or 1,
++        template_name='im/astakosgroup_list.html',
++        extra_context=dict(form=AstakosGroupSearchForm(),
++                           is_search=True,
++                           sorting=sorting))
  
  
  @signed_terms_required
@@@ -1043,22 -1031,111 +1053,74 @@@ def disapprove_member(request, membersh
  @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()
 -        
 -    data ={
 -        'resources':[{
 -            'name': 'vm',
 -            'description': 'Number Of Vms',
 -            'unit':'',
 -            'maxValue':'1',
 -            'currValue':'1'
 -            },{
 -            'name': 'ram',
 -            'description':'Total Ram Usage',
 -            'unit':'GB',
 -            'maxValue':'4',
 -            'currValue':'1' 
 -            },{
 -            'name': 'storage', 
 -            'description':'Total Disk Space Used',
 -            'unit':'GB',
 -            'maxValue':'200',
 -            'currValue':'180'             
 -            },{
 -            'name': 'disk', 
 -            'description':'Disks Used',
 -            'unit':'GB',
 -            'maxValue':'16',
 -            'currValue':'16'
 -            },{
 -            'name': 'network', 
 -            'description':'Private Networks Used',
 -            'unit':'',
 -            'maxValue':'2',
 -            'currValue':'1'
 -            },{
 -            'name': 'bandwidth', 
 -            'description':'Bandwidth Monitoring Device',
 -            'unit':'Gbps',
 -            'maxValue':'200',
 -            'currValue':'50'
 -            }]              
 -    } 
 -    
++#     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'])
 -        entry['ratio'] = (curr_value/max_value)*100
++        entry['ratio'] = (curr_value / max_value) * 100
+         if entry['ratio'] < 66:
 -            entry['load_class']='yellow'
++            entry['load_class'] = 'yellow'
+         if entry['ratio'] < 33:
 -            entry['load_class']='green'
 -        
 -        return entry 
 -    
 -    def pluralize(entry):
 -        if entry['unit'] == '':
 -            entry['plural'] = entry['name']+'s'
 -        else:
 -            entry['plural'] = entry['name']
 -        
 -        return entry       
++            entry['load_class'] = 'green'
 -    data['resources'] = map(with_class, data['resources']) 
 -    data['resources'] = map(pluralize, data['resources'])        
 -    
 -    return object_list(request, q,
 -                       template_name='im/astakosuserquota_list.html',
 -                       extra_context={'form': form, 'data':data})
++        return entry
++    def pluralize(entry):
++        entry['plural'] = engine.plural(entry.get('name'))
++        return entry
++    c = AstakosDjangoDBCallpoint()
++    try:
++        data = c.get_user_status(request.user.id)
++    except Exception, e:
++        data = None
++        messages.error(request, e)
 +    else:
-         form = PickResourceForm()
-         q = AstakosGroupQuota.objects.none()
-     return object_list(request, q,
-                        template_name='im/astakosuserquota_list.html',
-                        extra_context={'form': form})
++        backenddata = map(with_class, data)
++        data = map(pluralize, data)
++    return render_response('im/resource_list.html',
++                           data=data,
++                           context_instance=get_context(request))
+ def group_create_demo(request):
+     
+     resource_catalog = {
+         'groups': {
+             'compute': {
+                 'cyclades.vm':  { 'unit': 'number' }, 
+                 'cyclades.ram': { 'unit': 'bytes' }, 
+                 'cyclades.cpu': { 'unit': 'number' }
+             },
+             'storage': {
+                 'pithos.diskspace' : { 'unit': 'mebibytes' }
+             }
+         }
+     }
+     
+     
+     return render_response(
+         template='im/astakosgroup_form_demo.html',
+         context_instance=get_context(request),
+         resource_catalog=resource_catalog,
+         groups=resource_catalog['groups'] )
 -     
  
  
  def group_create_list(request):
  @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'])        
++    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]
--    
++        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)
++        data = _clear_billing_data(data)
          if status != 200:
              messages.error(request, _('Service response status: %d' % status))
      except:
          messages.error(request, r.result)
--    
++
      print type(start)
--    
++
      return render_response(
          template='im/billing.html',
          context_instance=get_context(request),
          data=data,
--        zerodate=datetime(month=1,year=1970, day=1),
++        zerodate=datetime(month=1, year=1970, day=1),
          today=today,
          start=int(start),
--        month_last_day=month_last_day)  
--    
++        month_last_day=month_last_day)
++
++
  def _clear_billing_data(data):
--    
++
      # remove addcredits entries
      def isnotcredit(e):
          return e['serviceName'] != "addcredits"
--    
--    
--    
--    # separate services    
++
++    # 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    
+     return data
 -
 -
+      
+      
  @signed_terms_required
  @login_required
  def timeline(request):
                                 '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'])
--        
++                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,
  #ASTAKOS_GREETING_EMAIL_SUBJECT = 'Welcome to %s alpha2 testing' % SITENAME
  #ASTAKOS_FEEDBACK_EMAIL_SUBJECT = 'Feedback from %s alpha2 testing' % SITENAME
  #ASTAKOS_VERIFICATION_EMAIL_SUBJECT = '%s alpha2 testing account activation is needed' % SITENAME
--#ASTAKOS_ADMIN_NOTIFICATION_EMAIL_SUBJECT = '%s alpha2 testing account created (%%(user)s)' % SITENAME
++#ASTAKOS_ACCOUNT_CREATION_SUBJECT = '%s alpha2 testing account created (%%(user)s)' % SITENAME)
++#ASTAKOS_GROUP_CREATION_SUBJECT = '%s alpha2 testing group created (%%(group)s)' % SITENAME)
  #ASTAKOS_HELPDESK_NOTIFICATION_EMAIL_SUBJECT = '%s alpha2 testing account activated (%%(user)s)' % SITENAME
  #ASTAKOS_EMAIL_CHANGE_EMAIL_SUBJECT = 'Email change on %s alpha2 testing' % SITENAME
  #ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT = 'Password reset on %s alpha2 testing' % SITENAME
index 0000000,0000000..74f827f
new file mode 100644 (file)
Binary files differ
index 0000000,0000000..772bea9
new file mode 100644 (file)
Binary files differ
@@@ -83,6 -83,6 +83,7 @@@ INSTALL_REQUIRES = 
      'commissioning',
      'celery',
      'requests',
++    'inflect'
  ]
  
  EXTRAS_REQUIRES = {