Project Notifications
[astakos] / snf-astakos-app / astakos / im / models.py
index 8deed6d..e77c3e3 100644 (file)
@@ -61,12 +61,18 @@ from django.contrib.auth.tokens import default_token_generator
 from django.conf import settings
 from django.utils.importlib import import_module
 from django.core.validators import email_re
+from django.core.exceptions import PermissionDenied
+from django.views.generic.create_update import lookup_object
+from django.core.exceptions import ObjectDoesNotExist
 
 from astakos.im.settings import (
     DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
     AUTH_TOKEN_DURATION, BILLING_FIELDS,
     EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
-    GROUP_CREATION_SUBJECT
+    SITENAME, SERVICES,
+    PROJECT_CREATION_SUBJECT, PROJECT_APPROVED_SUBJECT,
+    PROJECT_TERMINATION_SUBJECT, PROJECT_SUSPENSION_SUBJECT,
+    PROJECT_MEMBERSHIP_CHANGE_SUBJECT
 )
 from astakos.im.endpoints.qh import (
     register_users, send_quota, register_resources
@@ -83,10 +89,21 @@ import astakos.im.messages as astakos_messages
 logger = logging.getLogger(__name__)
 
 DEFAULT_CONTENT_TYPE = None
-try:
-    content_type = ContentType.objects.get(app_label='im', model='astakosuser')
-except:
-    content_type = DEFAULT_CONTENT_TYPE
+_content_type = None
+
+PENDING, APPROVED, REPLACED, UNKNOWN = 'Pending', 'Approved', 'Replaced', 'Unknown'
+
+def get_content_type():
+    global _content_type
+    if _content_type is not None:
+        return _content_type
+
+    try:
+        content_type = ContentType.objects.get(app_label='im', model='astakosuser')
+    except:
+        content_type = DEFAULT_CONTENT_TYPE
+    _content_type = content_type
+    return content_type
 
 RESOURCE_SEPARATOR = '.'
 
@@ -143,12 +160,15 @@ class ResourceMetadata(models.Model):
 
 
 class Resource(models.Model):
-    name = models.CharField(_('Name'), max_length=255, unique=True, db_index=True)
+    name = models.CharField(_('Name'), max_length=255)
     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)
+    
+    class Meta:
+        unique_together = ("name", "service")
 
     def __str__(self):
         return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
@@ -176,7 +196,10 @@ class AstakosGroup(Group):
         _('Creation date'),
         default=datetime.now()
     )
-    issue_date = models.DateTimeField('Start date', null=True)
+    issue_date = models.DateTimeField(
+        _('Start date'),
+        null=True
+    )
     expiration_date = models.DateTimeField(
         _('Expiration date'),
         null=True
@@ -300,7 +323,19 @@ class AstakosGroup(Group):
         self.owner = l
         map(self.approve_member, l)
 
-
+_default_quota = {}
+def get_default_quota():
+    global _default_quota
+    if _default_quota:
+        return _default_quota
+    for s, data in SERVICES.iteritems():
+        map(
+            lambda d:_default_quota.update(
+                {'%s%s%s' % (s, RESOURCE_SEPARATOR, d.get('name')):d.get('uplimit', 0)}
+            ),
+            data.get('resources', {})
+        )
+    return _default_quota
 
 class AstakosUserManager(UserManager):
 
@@ -398,16 +433,17 @@ class AstakosUser(User):
     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)
+        p, created = Permission.objects.get_or_create(
+                                    codename=pname,
+                                    name=pname.capitalize(),
+                                    content_type=get_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)
+                                   content_type=get_content_type())
         self.user_permissions.remove(p)
 
     @property
@@ -428,16 +464,19 @@ class AstakosUser(User):
     def quota(self):
         """Returns a dict with the sum of quota limits per resource"""
         d = defaultdict(int)
+        default_quota = get_default_quota()
+        d.update(default_quota)
         for q in self.policies:
             d[q.resource] += q.uplimit or inf
-        for m in self.extended_groups:
-            if not m.is_approved:
+        for m in self.projectmembership_set.select_related().all():
+            if not m.acceptance_date:
                 continue
-            g = m.group
-            if not g.is_enabled:
+            p = m.project
+            if not p.is_active:
                 continue
-            for r, uplimit in g.quota.iteritems():
-                d[r] += uplimit or inf
+            grants = p.application.definition.projectresourcegrant_set.all()
+            for g in grants:
+                d[str(g.resource)] += g.member_limit or inf
         # TODO set default for remaining
         return d
 
@@ -1000,17 +1039,67 @@ class SessionCatalog(models.Model):
     session_key = models.CharField(_('session key'), max_length=40)
     user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
 
-class MemberAcceptPolicy(models.Model):
+class MemberJoinPolicy(models.Model):
     policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
     description = models.CharField(_('Description'), max_length=80)
 
     def __str__(self):
         return self.policy
 
-try:
-    auto_accept = MemberAcceptPolicy.objects.get(policy='auto_accept')
-except:
-    auto_accept = None
+class MemberLeavePolicy(models.Model):
+    policy = models.CharField(_('Policy'), max_length=255, unique=True, db_index=True)
+    description = models.CharField(_('Description'), max_length=80)
+
+    def __str__(self):
+        return self.policy
+
+_auto_accept_join = False
+def get_auto_accept_join():
+    global _auto_accept_join
+    if _auto_accept_join is not False:
+        return _auto_accept_join
+    try:
+        auto_accept = MemberJoinPolicy.objects.get(policy='auto_accept')
+    except:
+        auto_accept = None
+    _auto_accept_join = auto_accept
+    return auto_accept
+
+_closed_join = False
+def get_closed_join():
+    global _closed_join
+    if _closed_join is not False:
+        return _closed_join
+    try:
+        closed = MemberJoinPolicy.objects.get(policy='closed')
+    except:
+        closed = None
+    _closed_join = closed
+    return closed
+
+_auto_accept_leave = False
+def get_auto_accept_leave():
+    global _auto_accept_leave
+    if _auto_accept_leave is not False:
+        return _auto_accept_leave
+    try:
+        auto_accept = MemberLeavePolicy.objects.get(policy='auto_accept')
+    except:
+        auto_accept = None
+    _auto_accept_leave = auto_accept
+    return auto_accept
+
+_closed_leave = False
+def get_closed_leave():
+    global _closed_leave
+    if _closed_leave is not False:
+        return _closed_leave
+    try:
+        closed = MemberLeavePolicy.objects.get(policy='closed')
+    except:
+        closed = None
+    _closed_leave = closed
+    return closeds
 
 class ProjectDefinition(models.Model):
     name = models.CharField(max_length=80)
@@ -1018,7 +1107,8 @@ class ProjectDefinition(models.Model):
     description = models.TextField(null=True)
     start_date = models.DateTimeField()
     end_date = models.DateTimeField()
-    member_accept_policy = models.ForeignKey(MemberAcceptPolicy)
+    member_join_policy = models.ForeignKey(MemberJoinPolicy)
+    member_leave_policy = models.ForeignKey(MemberLeavePolicy)
     limit_on_members_number = models.PositiveIntegerField(null=True,blank=True)
     resource_grants = models.ManyToManyField(
         Resource,
@@ -1027,19 +1117,6 @@ class ProjectDefinition(models.Model):
         through='ProjectResourceGrant'
     )
     
-    def save(self):
-        self.validate_name()
-        super(ProjectDefinition, self).save()
-    
-    def validate_name(self):
-        """
-        Validate name uniqueness among all active projects.
-        """
-        alive_projects = list(get_alive_projects())
-        q = filter(lambda p: p.definition.name==self.name, alive_projects)
-        if q:
-            raise ValidationError({'name': [_(astakos_messages.UNIQUE_PROJECT_NAME_CONSTRAIN_ERR)]})
-    
     @property
     def violated_resource_grants(self):
         return False
@@ -1070,8 +1147,20 @@ class ProjectDefinition(models.Model):
             update = p.get('update', True)
             self.add_resource_policy(service, resource, uplimit, update)
     
-    def get_absolute_url(self):
-        return reverse('project_application_detail', args=(self.serial,))
+    def validate_name(self):
+        """
+        Validate name uniqueness among all active projects.
+        """
+        alive_projects = list(get_alive_projects())
+        q = filter(
+            lambda p: p.definition.name == self.name and \
+                p.application.id != self.projectapplication.id,
+            alive_projects
+        )
+        if q:
+            raise ValidationError(
+                _(astakos_messages.UNIQUE_PROJECT_NAME_CONSTRAIN_ERR)
+            )
 
 
 class ProjectResourceGrant(models.Model):
@@ -1084,82 +1173,199 @@ class ProjectResourceGrant(models.Model):
     class Meta:
         unique_together = ("resource", "project_definition")
 
+
 class ProjectApplication(models.Model):
-    serial = models.CharField(
-        primary_key=True,
-        max_length=30,
-        unique=True,
-        default=uuid.uuid4().hex[:30]
+    states_list = [PENDING, APPROVED, REPLACED, UNKNOWN]
+    states = dict((k, v) for k, v in enumerate(states_list))
+
+    applicant = models.ForeignKey(
+        AstakosUser,
+        related_name='my_project_applications',
+        db_index=True)
+    owner = models.ForeignKey(
+        AstakosUser,
+        related_name='own_project_applications',
+        db_index=True
     )
-    applicant = models.ForeignKey(AstakosUser, related_name='my_project_applications')
-    owner = models.ForeignKey(AstakosUser, related_name='own_project_applications')
     comments = models.TextField(null=True, blank=True)
     definition = models.OneToOneField(ProjectDefinition)
     issue_date = models.DateTimeField()
     precursor_application = models.OneToOneField('ProjectApplication',
         null=True,
-        blank=True
+        blank=True,
+        db_index=True
     )
+    state = models.CharField(max_length=80, default=UNKNOWN)
+    
+    @property
+    def follower(self):
+        try:
+            return ProjectApplication.objects.get(precursor_application=self)
+        except ProjectApplication.DoesNotExist:
+            return
+
+    def save(self):
+        self.definition.save()
+        self.definition = self.definition
+        super(ProjectApplication, self).save()
+
+
+    @staticmethod
+    def submit(definition, resource_policies, applicant, comments,
+               precursor_application=None, commit=True):
+
+        application = ProjectApplication()
+        if precursor_application:
+            application.precursor_application = precursor_application
+            application.owner = precursor_application.owner
+        else:
+            application.owner = applicant
+
+        application.definition = definition
+        application.definition.resource_policies = resource_policies
+        application.applicant = applicant
+        application.comments = comments
+        application.issue_date = datetime.now()
+        application.state = PENDING
+
+        if commit:
+            application.save()
+            application.definition.resource_policies = resource_policies
+            # better implementation ???
+            if precursor_application:
+                try:
+                    precursor = ProjectApplication.objects.get(id=precursor_application_id)
+                except:
+                    pass
+                application.precursor_application = precursor
+                application.save()
+
+        notification = build_notification(
+            settings.SERVER_EMAIL,
+            [i[1] for i in settings.ADMINS],
+            _(PROJECT_CREATION_SUBJECT) % application.definition.__dict__,
+            template='im/projects/project_creation_notification.txt',
+            dictionary={'object':application}
+        )
+        notification.send()
+        return application
+
+    def approve(self, approval_user=None):
+        """
+        If approval_user then during owner membership acceptance
+        it is checked whether the request_user is eligible.
+
+        Raises:
+            ValidationError: if there is other alive project with the same name
+
+        """
+        try:
+            self.definition.validate_name()
+        except ValidationError, e:
+            raise PermissionDenied(e.messages[0])
+        if self.state != PENDING:
+            raise PermissionDenied(_(PROJECT_ALREADY_ACTIVE))
+
+        try:
+            precursor = self.precursor_application
+            project = precursor.project
+            project.application = self
+            prev_approval_date = project.last_approval_date
+            project.last_approval_date = datetime.now()
+            project.save()
+
+            p = precursor
+            while p:
+                p.state = REPLACED
+                p.save()
+                p = p.precursor_application
+
+        except:
+            kwargs = {
+                'application':self,
+                'creation_date':datetime.now(),
+                'last_approval_date':datetime.now(),
+            }
+            project = _create_object(Project, **kwargs)
+            project.accept_member(self.owner, approval_user)
+            precursor = None
+
+        self.state = APPROVED
+        self.save()
+
+        notification = build_notification(
+            settings.SERVER_EMAIL,
+            [self.owner.email],
+            _(PROJECT_APPROVED_SUBJECT) % self.definition.__dict__,
+            template='im/projects/project_approval_notification.txt',
+            dictionary={'object':self}
+        )
+        notification.send()
+
+        rejected = self.project.sync()
+        if rejected:
+            # revert to precursor
+            if precursor:
+                project.application = precursor
+                project.last_approval_date = prev_approval_date
+                project.save()
+
+            rejected = project.sync()
+            if rejected:
+                raise Exception(_(astakos_messages.QH_SYNC_ERROR))
+        else:
+            project.last_application_synced = self
+            project.save()
+
 
 class Project(models.Model):
-    serial = models.CharField(
-        _('username'),
-        primary_key=True,
-        max_length=30,
-        unique=True,
-        default=uuid.uuid4().hex[:30]
-    )
     application = models.OneToOneField(ProjectApplication, related_name='project')
     creation_date = models.DateTimeField()
-    last_approval_date = models.DateTimeField()
-    termination_date = models.DateTimeField()
+    last_approval_date = models.DateTimeField(null=True)
+    termination_start_date = models.DateTimeField(null=True)
+    termination_date = models.DateTimeField(null=True)
     members = models.ManyToManyField(AstakosUser, through='ProjectMembership')
-    last_synced_application = models.OneToOneField(
+    membership_dirty = models.BooleanField(default=False)
+    last_application_synced = models.OneToOneField(
         ProjectApplication, related_name='last_project', null=True, blank=True
     )
     
+    
     @property
     def definition(self):
         return self.application.definition
-    
+
     @property
-    def is_valid(self):
-        try:
-            self.application.definition.validate_name()
-        except ValidationError:
-            return False
-        else:
-            return True
-    
+    def violated_members_number_limit(self):
+        return len(self.approved_members) <= self.definition.limit_on_members_number
+        
     @property
     def is_active(self):
-        if not self.is_valid:
-            return False
         if not self.last_approval_date:
             return False
         if self.termination_date:
             return False
         if self.definition.violated_resource_grants:
             return False
+#         if self.violated_members_number_limit:
+#             return False
         return True
     
     @property
     def is_terminated(self):
-        if not self.is_valid:
-            return False
         if not self.termination_date:
             return False
         return True
     
     @property
     def is_suspended(self):
-        if not self.is_valid:
-            return False
-        if not self.termination_date:
+        if self.termination_date:
             return False
-        if not self.last_approval_date:
+        if self.last_approval_date:
             if not self.definition.violated_resource_grants:
                 return False
+#             if not self.violated_members_number_limit:
+#                 return False
         return True
     
     @property
@@ -1178,34 +1384,231 @@ class Project(models.Model):
         return False
     
     @property
+    def is_synchronized(self):
+        return self.last_application_synced == self.application and \
+            not self.membership_dirty and \
+            (not self.termination_start_date or termination_date)
+    
+    @property
     def approved_members(self):
-        return [m.person for m in self.members.filter(is_accepted=True)]
+        return [m.person for m in self.projectmembership_set.filter(~Q(acceptance_date=None))]
+        
+    def sync(self, specific_members=()):
+        if self.is_synchronized:
+            return
+        members = specific_members or self.approved_members
+        c, rejected = send_quota(self.approved_members)
+        return rejected
     
-    def suspend(self):
-        self.last_approval_date = None
-        self.save()
+    def accept_member(self, user, request_user=None):
+        """
+        Raises:
+            django.exceptions.PermissionDenied
+            astakos.im.models.AstakosUser.DoesNotExist
+        """
+        if isinstance(user, int):
+            try:
+                user = lookup_object(AstakosUser, user, None, None)
+            except Http404:
+                raise AstakosUser.DoesNotExist()
+        m, created = ProjectMembership.objects.get_or_create(
+            person=user, project=self
+        )
+        m.accept(delete_on_failure=created, request_user=None)
+
+    def reject_member(self, user, request_user=None):
+        """
+        Raises:
+            django.exceptions.PermissionDenied
+            astakos.im.models.AstakosUser.DoesNotExist
+            astakos.im.models.ProjectMembership.DoesNotExist
+        """
+        if isinstance(user, int):
+            try:
+                user = lookup_object(AstakosUser, user, None, None)
+            except Http404:
+                raise AstakosUser.DoesNotExist()
+        m = ProjectMembership.objects.get(person=user, project=self)
+        m.reject()
+        
+    def remove_member(self, user, request_user=None):
+        """
+        Raises:
+            django.exceptions.PermissionDenied
+            astakos.im.models.AstakosUser.DoesNotExist
+            astakos.im.models.ProjectMembership.DoesNotExist
+        """
+        if isinstance(user, int):
+            try:
+                user = lookup_object(AstakosUser, user, None, None)
+            except Http404:
+                raise AstakosUser.DoesNotExist()
+        m = ProjectMembership.objects.get(person=user, project=self)
+        m.remove()
     
     def terminate(self):
-        self.terminaton_date = datetime.now()
+        self.termination_start_date = datetime.now()
+        self.terminaton_date = None
         self.save()
-    
-    def sync(self):
-        c, rejected = send_quota(self.approved_members)
-        return rejected
+        
+        rejected = self.sync()
+        if not rejected:
+            self.termination_start_date = None
+            self.terminaton_date = datetime.now()
+            self.save()
+            
+        notification = build_notification(
+            settings.SERVER_EMAIL,
+            [self.application.owner.email],
+            _(PROJECT_TERMINATION_SUBJECT) % self.definition.__dict__,
+            template='im/projects/project_termination_notification.txt',
+            dictionary={'object':self.application}
+        )
+        notification.send()
+
+    def suspend(self):
+        self.last_approval_date = None
+        self.save()
+        self.sync()
+        notification = build_notification(
+            settings.SERVER_EMAIL,
+            [self.application.owner.email],
+            _(PROJECT_SUSPENSION_SUBJECT) % self.definition.__dict__,
+            template='im/projects/project_suspension_notification.txt',
+            dictionary={'object':self.application}
+        )
+        notification.send()
 
 class ProjectMembership(models.Model):
     person = models.ForeignKey(AstakosUser)
     project = models.ForeignKey(Project)
-    issue_date = models.DateField(default=datetime.now())
-    decision_date = models.DateField(null=True, db_index=True)
-    is_accepted = models.BooleanField(
-        _('Whether the membership application is accepted'),
-        default=False
-    )
+    request_date = models.DateField(default=datetime.now())
+    acceptance_date = models.DateField(null=True, db_index=True)
+    leave_request_date = models.DateField(null=True)
 
     class Meta:
         unique_together = ("person", "project")
 
+    def accept(self, delete_on_failure=False, request_user=None):
+        """
+            Raises:
+                django.exception.PermissionDenied
+                astakos.im.notifications.NotificationError
+        """
+        try:
+            if request_user and \
+                (not self.project.application.owner == request_user and \
+                    not request_user.is_superuser):
+                raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
+            if not self.project.is_alive:
+                raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % self.project.__dict__)
+            if self.project.definition.member_join_policy == 'closed':
+                raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
+            if len(self.project.approved_members) + 1 > self.project.definition.limit_on_members_number:
+                raise PermissionDenied(_(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED))
+        except PermissionDenied, e:
+            if delete_on_failure:
+                m.delete()
+            raise
+        if self.acceptance_date:
+            return
+        self.acceptance_date = datetime.now()
+        self.save()
+        notification = build_notification(
+            settings.SERVER_EMAIL,
+            [self.person.email],
+            _(PROJECT_MEMBERSHIP_CHANGE_SUBJECT) % self.project.definition.__dict__,
+            template='im/projects/project_membership_change_notification.txt',
+            dictionary={'object':self.project.application, 'action':'accepted'}
+        ).send()
+        self.sync()
+    
+    def reject(self, request_user=None):
+        """
+            Raises:
+                django.exception.PermissionDenied,
+                astakos.im.notifications.NotificationError
+        """
+        if request_user and \
+            (not self.project.application.owner == request_user and \
+                not request_user.is_superuser):
+            raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
+        if not self.project.is_alive:
+            raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
+        history_item = ProjectMembershipHistory(
+            person=self.person,
+            project=self.project,
+            request_date=self.request_date,
+            rejection_date=datetime.now()
+        )
+        self.delete()
+        history_item.save()
+        notification = build_notification(
+            settings.SERVER_EMAIL,
+            [self.person.email],
+            _(PROJECT_MEMBERSHIP_CHANGE_SUBJECT) % self.project.definition.__dict__,
+            template='im/projects/project_membership_change_notification.txt',
+            dictionary={'object':self.project.application, 'action':'rejected'}
+        ).send()
+    
+    def remove(self, request_user=None):
+        """
+            Raises:
+                django.exception.PermissionDenied
+                astakos.im.notifications.NotificationError
+        """
+        if request_user and \
+            (not self.project.application.owner == request_user and \
+                not request_user.is_superuser):
+            raise PermissionDenied(_(astakos_messages.NOT_ALLOWED))
+        if not self.project.is_alive:
+            raise PermissionDenied(_(astakos_messages.NOT_ALIVE_PROJECT) % self.project.__dict__)
+        history_item = ProjectMembershipHistory(
+            id=self.id,
+            person=self.person,
+            project=self.project,
+            request_date=self.request_date,
+            removal_date=datetime.now()
+        )
+        self.delete()
+        history_item.save()
+        notification = build_notification(
+            settings.SERVER_EMAIL,
+            [self.person.email],
+            _(PROJECT_MEMBERSHIP_CHANGE_SUBJECT) % self.project.definition.__dict__,
+            template='im/projects/project_membership_change_notification.txt',
+            dictionary={'object':self.project.application, 'action':'removed'}
+        ).send()
+        self.sync()
+    
+    def leave(self):
+        leave_policy = self.project.application.definition.member_leave_policy
+        if leave_policy == get_auto_accept_leave():
+            self.remove()
+        else:
+            self.leave_request_date = datetime.now()
+            self.save()
+
+    def sync(self):
+        # set membership_dirty flag
+        self.project.membership_dirty = True
+        self.project.save()
+        
+        rejected = self.project.sync(specific_members=[self.person])
+        if not rejected:
+            # if syncing was successful unset membership_dirty flag
+            self.membership_dirty = False
+            self.project.save()
+        
+
+class ProjectMembershipHistory(models.Model):
+    person = models.ForeignKey(AstakosUser)
+    project = models.ForeignKey(Project)
+    request_date = models.DateField(default=datetime.now())
+    removal_date = models.DateField(null=True)
+    rejection_date = models.DateField(null=True)
+
+
 def filter_queryset_by_property(q, property):
     """
     Incorporate list comprehension for filtering querysets by property
@@ -1225,181 +1628,12 @@ def get_active_projects():
         'is_active'
     )
 
-def _lookup_object(model, **kwargs):
-    """
-    Returns an object of the specific model matching the given lookup
-    parameters.
-    """
-    if not kwargs:
-        raise MissingIdentifier
-    try:
-        return model.objects.get(**kwargs)
-    except model.DoesNotExist:
-        raise ItemNotExists(model._meta.verbose_name, **kwargs)
-    except model.MultipleObjectsReturned:
-        raise MultipleItemsExist(model._meta.verbose_name, **kwargs)
-
 def _create_object(model, **kwargs):
     o = model.objects.create(**kwargs)
     o.save()
     return o
 
-def _update_object(model, id, save=True, **kwargs):
-    o = self._lookup_object(model, id=id)
-    if kwargs:
-        o.__dict__.update(kwargs)
-    if save:
-        o.save()
-    return o
-
-def list_applications():
-    return ProjectAppication.objects.all()
-
-def submit_application(definition, applicant, comments, precursor_application=None, commit=True):
-    if precursor_application:
-        application = precursor_application.copy()
-        application.precursor_application = precursor_application
-    else:
-        application = ProjectApplication(owner=applicant)
-    application.definition = definition
-    application.applicant = applicant
-    application.comments = comments
-    application.issue_date = datetime.now()
-    if commit:
-        definition.save()
-        application.save()
-        notification = build_notification(
-        settings.SERVER_EMAIL,
-        [i[1] for i in settings.ADMINS],
-        _(GROUP_CREATION_SUBJECT) % {'group':application.definition.name},
-        _('An new project application identified by %(serial)s has been submitted.') % application.__dict__
-    )
-    notification.send()
-    return application
-    
-def approve_application(serial):
-    app = _lookup_object(ProjectAppication, serial=serial)
-    notify = False
-    if not app.precursor_application:
-        kwargs = {
-            'application':app,
-            'creation_date':datetime.now(),
-            'last_approval_date':datetime.now(),
-        }
-        project = _create_object(Project, **kwargs)
-    else:
-        project = app.precursor_application.project
-        last_approval_date = project.last_approval_date
-        if project.is_valid:
-            project.application = app
-            project.last_approval_date = datetime.now()
-            project.save()
-        else:
-            raise Exception(_(astakos_messages.INVALID_PROJECT) % project.__dict__)
-    
-    rejected = synchonize_project(project.serial)
-    if rejected:
-        # revert to precursor
-        project.appication = app.precursor_application
-        if project.application:
-            project.last_approval_date = last_approval_date
-        project.save()
-        rejected = synchonize_project(project.serial)
-        if rejected:
-            raise Exception(_(astakos_messages.QH_SYNC_ERROR))
-    else:
-        project.last_application_synced = app
-        project.save()
-        sender, recipients, subject, message
-        notification = build_notification(
-            settings.SERVER_EMAIL,
-            [project.owner.email],
-            _('Project application has been approved on %s alpha2 testing' % SITENAME),
-            _('Your application request %(serial)s has been apporved.')
-        )
-        notification.send()
-
-
-def list_projects(filter_property=None):
-    if filter_property:
-        return filter_queryset_by_property(
-            Project.objects.all(),
-            filter_property
-        )
-    return Project.objects.all()
-
-def add_project_member(serial, user_id, request_user):
-    project = _lookup_object(Project, serial=serial)
-    user = _lookup_object(AstakosUser, id=user_id)
-    if not project.owner == request_user:
-        raise Exception(_(astakos_messages.NOT_PROJECT_OWNER))
-    
-    if not project.is_alive:
-        raise Exception(_(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
-    if len(project.members) + 1 > project.limit_on_members_number:
-        raise Exception(_(astakos_messages.MEMBER_NUMBER_LIMIT_REACHED))
-    m = self._lookup_object(ProjectMembership, person=user, project=project)
-    if m.is_accepted:
-        return
-    m.is_accepted = True
-    m.decision_date = datetime.now()
-    m.save()
-    notification = build_notification(
-        settings.SERVER_EMAIL,
-        [user.email],
-        _('Your membership on project %(name)s has been accepted.') % project.definition.__dict__, 
-        _('Your membership on project %(name)s has been accepted.') % project.definition.__dict__,
-    )
-    notification.send()
-
-def remove_project_member(serial, user_id, request_user):
-    project = _lookup_object(Project, serial=serial)
-    if not project.is_alive:
-        raise Exception(_(astakos_messages.NOT_ALIVE_PROJECT) % project.__dict__)
-    if not project.owner == request_user:
-        raise Exception(_(astakos_messages.NOT_PROJECT_OWNER))
-    user = self.lookup_user(user_id)
-    m = _lookup_object(ProjectMembership, person=user, project=project)
-    if not m.is_accepted:
-        return
-    m.is_accepted = False
-    m.decision_date = datetime.now()
-    m.save()
-    notification = build_notification(
-        settings.SERVER_EMAIL,
-        [user.email],
-        _('Your membership on project %(name)s has been removed.') % project.definition.__dict__,
-        _('Your membership on project %(name)s has been removed.') % project.definition.__dict__
-    )
-    notification.send()    
-
-def suspend_project(serial):
-    project = _lookup_object(Project, serial=serial)
-    project.suspend()
-    notification = build_notification(
-        settings.SERVER_EMAIL,
-        [project.owner.email],
-        _('Project %(name)s has been suspended.') %  project.definition.__dict__,
-        _('Project %(name)s has been suspended.') %  project.definition.__dict__
-    )
-    notification.send()
-
-def terminate_project(serial):
-    project = _lookup_object(Project, serial=serial)
-    project.termination()
-    notification = build_notification(
-        settings.SERVER_EMAIL,
-        [project.owner.email],
-        _('Project %(name)s has been terminated.') %  project.definition.__dict__,
-        _('Project %(name)s has been terminated.') %  project.definition.__dict__
-    )
-    notification.send()
 
-def synchonize_project(serial):
-    project = _lookup_object(Project, serial=serial)
-    if project.app != project.last_application_synced:
-        return project.sync()
-     
 def create_astakos_user(u):
     try:
         AstakosUser.objects.get(user_ptr=u.pk)
@@ -1418,21 +1652,14 @@ def fix_superusers(sender, **kwargs):
     admins = User.objects.filter(is_superuser=True)
     for u in admins:
         create_astakos_user(u)
+post_syncdb.connect(fix_superusers)
 
 
 def user_post_save(sender, instance, created, **kwargs):
     if not created:
         return
     create_astakos_user(instance)
-
-
-def set_default_group(user):
-    try:
-        default = AstakosGroup.objects.get(name='default')
-        Membership(
-            group=default, person=user, date_joined=datetime.now()).save()
-    except AstakosGroup.DoesNotExist, e:
-        logger.exception(e)
+post_save.connect(user_post_save, sender=User)
 
 
 def astakosuser_pre_save(sender, instance, **kwargs):
@@ -1449,6 +1676,15 @@ def astakosuser_pre_save(sender, instance, **kwargs):
         l = filter(lambda f: get(db_instance, f) != get(instance, f),
                    BILLING_FIELDS)
         instance.aquarium_report = True if l else False
+pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
+
+def set_default_group(user):
+    try:
+        default = AstakosGroup.objects.get(name='default')
+        Membership(
+            group=default, person=user, date_joined=datetime.now()).save()
+    except AstakosGroup.DoesNotExist, e:
+        logger.exception(e)
 
 
 def astakosuser_post_save(sender, instance, created, **kwargs):
@@ -1459,12 +1695,24 @@ def astakosuser_post_save(sender, instance, created, **kwargs):
     set_default_group(instance)
     # TODO handle socket.error & IOError
     register_users((instance,))
+post_save.connect(astakosuser_post_save, sender=AstakosUser)
 
 
 def resource_post_save(sender, instance, created, **kwargs):
     if not created:
         return
     register_resources((instance,))
+post_save.connect(resource_post_save, sender=Resource)
+
+
+def on_quota_disturbed(sender, users, **kwargs):
+#     print '>>>', locals()
+    if not users:
+        return
+    send_quota(users)
+
+quota_disturbed = Signal(providing_args=["users"])
+quota_disturbed.connect(on_quota_disturbed)
 
 
 def send_quota_disturbed(sender, instance, **kwargs):
@@ -1484,27 +1732,6 @@ def send_quota_disturbed(sender, instance, **kwargs):
         if not instance.is_enabled:
             return
     quota_disturbed.send(sender=sender, users=users)
-
-
-def on_quota_disturbed(sender, users, **kwargs):
-#     print '>>>', locals()
-    if not users:
-        return
-    send_quota(users)
-
-def renew_token(sender, instance, **kwargs):
-    if not instance.auth_token:
-        instance.renew_token()
-
-post_syncdb.connect(fix_superusers)
-post_save.connect(user_post_save, sender=User)
-pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
-post_save.connect(astakosuser_post_save, sender=AstakosUser)
-post_save.connect(resource_post_save, sender=Resource)
-
-quota_disturbed = Signal(providing_args=["users"])
-quota_disturbed.connect(on_quota_disturbed)
-
 post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
 post_delete.connect(send_quota_disturbed, sender=Membership)
 post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
@@ -1512,5 +1739,27 @@ post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
 post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
 post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
 
+
+def renew_token(sender, instance, **kwargs):
+    if not instance.auth_token:
+        instance.renew_token()
 pre_save.connect(renew_token, sender=AstakosUser)
 pre_save.connect(renew_token, sender=Service)
+
+
+def check_closed_join_membership_policy(sender, instance, **kwargs):
+    if instance.id:
+        return
+    join_policy = instance.project.application.definition.member_join_policy
+    if join_policy == get_closed_join():
+        raise PermissionDenied(_(astakos_messages.MEMBER_JOIN_POLICY_CLOSED))
+pre_save.connect(check_closed_join_membership_policy, sender=ProjectMembership)
+
+
+def check_auto_accept_join_membership_policy(sender, instance, created, **kwargs):
+    if not created:
+        return
+    join_policy = instance.project.application.definition.member_join_policy
+    if join_policy == get_auto_accept_join():
+        instance.accept()
+post_save.connect(check_auto_accept_join_membership_policy, sender=ProjectMembership)
\ No newline at end of file