commit 6d583e07b63f09fd294637f20771b08d1482fa35
Author: Giorgos Korfiatis <gkorf@grnet.gr>
Date:   Wed Jun 19 10:50:24 2013 +0300

    astakos: Simplify project schema
    
    Applications now point to a project (rather than an extra model Chain).
    A Project is created upon submitting an application; it always points to
    a single `reference' application.
    
    Model Chain is kept as a sequence to generate project ids.

diff --git a/snf-astakos-app/astakos/im/models.py b/snf-astakos-app/astakos/im/models.py
index 9938c51..695c010 100644
--- a/snf-astakos-app/astakos/im/models.py
+++ b/snf-astakos-app/astakos/im/models.py
@@ -1280,142 +1280,13 @@ class UserSetting(models.Model):
 ### PROJECTS ###
 ################
 
-class ChainManager(ForUpdateManager):
-
-    def search_by_name(self, *search_strings):
-        projects = Project.objects.search_by_name(*search_strings)
-        chains = [p.id for p in projects]
-        apps = ProjectApplication.objects.search_by_name(*search_strings)
-        apps = (app for app in apps if app.is_latest())
-        app_chains = [app.chain for app in apps if app.chain not in chains]
-        return chains + app_chains
-
-    def all_full_state(self):
-        chains = self.all()
-        cids = [c.chain for c in chains]
-        projects = Project.objects.select_related('application').in_bulk(cids)
-
-        objs = Chain.objects.annotate(latest=Max('chained_apps__id'))
-        chain_latest = dict(objs.values_list('chain', 'latest'))
-
-        objs = ProjectApplication.objects.select_related('applicant')
-        apps = objs.in_bulk(chain_latest.values())
-
-        d = {}
-        for chain in chains:
-            pk = chain.pk
-            project = projects.get(pk, None)
-            app = apps[chain_latest[pk]]
-            d[chain.pk] = chain.get_state(project, app)
-
-        return d
-
-    def of_project(self, project):
-        if project is None:
-            return None
-        try:
-            return self.get(chain=project.id)
-        except Chain.DoesNotExist:
-            raise AssertionError('project with no chain')
-
-
 class Chain(models.Model):
     chain = models.AutoField(primary_key=True)
+    objects = ForUpdateManager()
 
     def __str__(self):
         return "%s" % (self.chain,)
 
-    objects = ChainManager()
-
-    PENDING = 0
-    DENIED = 3
-    DISMISSED = 4
-    CANCELLED = 5
-
-    APPROVED = 10
-    APPROVED_PENDING = 11
-    SUSPENDED = 12
-    SUSPENDED_PENDING = 13
-    TERMINATED = 14
-    TERMINATED_PENDING = 15
-
-    PENDING_STATES = [PENDING,
-                      APPROVED_PENDING,
-                      SUSPENDED_PENDING,
-                      TERMINATED_PENDING,
-                      ]
-
-    MODIFICATION_STATES = [APPROVED_PENDING,
-                           SUSPENDED_PENDING,
-                           TERMINATED_PENDING,
-                           ]
-
-    RELEVANT_STATES = [PENDING,
-                       DENIED,
-                       APPROVED,
-                       APPROVED_PENDING,
-                       SUSPENDED,
-                       SUSPENDED_PENDING,
-                       TERMINATED_PENDING,
-                       ]
-
-    SKIP_STATES = [DISMISSED,
-                   CANCELLED,
-                   TERMINATED]
-
-    STATE_DISPLAY = {
-        PENDING:             _("Pending"),
-        DENIED:              _("Denied"),
-        DISMISSED:           _("Dismissed"),
-        CANCELLED:           _("Cancelled"),
-        APPROVED:            _("Active"),
-        APPROVED_PENDING:    _("Active - Pending"),
-        SUSPENDED:           _("Suspended"),
-        SUSPENDED_PENDING:   _("Suspended - Pending"),
-        TERMINATED:          _("Terminated"),
-        TERMINATED_PENDING:  _("Terminated - Pending"),
-    }
-
-    @classmethod
-    def _chain_state(cls, project_state, app_state):
-        s = CHAIN_STATE.get((project_state, app_state), None)
-        if s is None:
-            raise AssertionError('inconsistent chain state')
-        return s
-
-    @classmethod
-    def chain_state(cls, project, app):
-        p_state = project.state if project else None
-        return cls._chain_state(p_state, app.state)
-
-    @classmethod
-    def state_display(cls, s):
-        if s is None:
-            return _("Unknown")
-        return cls.STATE_DISPLAY.get(s, _("Inconsistent"))
-
-    def last_application(self):
-        return self.chained_apps.order_by('-id')[0]
-
-    def get_project(self):
-        try:
-            return self.chained_project
-        except Project.DoesNotExist:
-            return None
-
-    def get_elements(self):
-        project = self.get_project()
-        app = self.last_application()
-        return project, app
-
-    def get_state(self, project, app):
-        s = self.chain_state(project, app)
-        return s, project, app
-
-    def full_state(self):
-        project, app = self.get_elements()
-        return self.get_state(project, app)
-
 
 def new_chain():
     c = Chain.objects.create()
@@ -1482,7 +1353,7 @@ class ProjectApplication(models.Model):
         AstakosUser,
         related_name='projects_owned',
         db_index=True)
-    chain = models.ForeignKey(Chain,
+    chain = models.ForeignKey('Project',
                               related_name='chained_apps',
                               db_column='chain')
     precursor_application = models.ForeignKey('ProjectApplication',
@@ -1625,15 +1496,6 @@ class ProjectApplication(models.Model):
         except Project.DoesNotExist:
             return False
 
-    def get_project(self):
-        try:
-            return Project.objects.get(id=self.chain)
-        except Project.DoesNotExist:
-            return None
-
-    def project_exists(self):
-        return self.get_project() is not None
-
     def can_cancel(self):
         return self.state == self.PENDING
 
@@ -1687,18 +1549,6 @@ class ProjectApplication(models.Model):
         self.response = reason
         self.save()
 
-        project = self.get_project()
-        if project is None:
-            project = Project(id=self.chain)
-
-        project.name = self.name
-        project.application = self
-        project.last_approval_date = now
-        project.save()
-        if project.is_deactivated():
-            project.resume()
-        return project
-
     @property
     def member_join_policy_display(self):
         policy = self.member_join_policy
@@ -1765,38 +1615,46 @@ class ProjectResourceGrant(models.Model):
                 return '0'
 
 
-class ProjectManager(ForUpdateManager):
+def _distinct(f, l):
+    d = {}
+    last = None
+    for x in l:
+        group = f(x)
+        if group == last:
+            continue
+        last = group
+        d[group] = x
+    return d
 
-    def terminated_projects(self):
-        q = self.model.Q_TERMINATED
-        return self.filter(q)
 
-    def not_terminated_projects(self):
-        q = ~self.model.Q_TERMINATED
-        return self.filter(q)
+def invert_dict(d):
+    return dict((v, k) for k, v in d.iteritems())
 
-    def deactivated_projects(self):
-        q = self.model.Q_DEACTIVATED
-        return self.filter(q)
+
+class ProjectManager(ForUpdateManager):
+
+    def all_with_pending(self, flt=None):
+        flt = Q() if flt is None else flt
+        projects = list(self.select_related(
+            'application', 'application__owner').filter(flt))
+
+        objs = ProjectApplication.objects.select_related('owner')
+        apps = objs.filter(state=ProjectApplication.PENDING,
+                           chain__in=projects).order_by('chain', '-id')
+        app_d = _distinct(lambda app: app.chain_id, apps)
+        return [(project, app_d.get(project.pk)) for project in projects]
 
     def expired_projects(self):
-        q = (~Q(state=Project.TERMINATED) &
+        model = self.model
+        q = ((model.o_state_q(model.O_ACTIVE) |
+              model.o_state_q(model.O_SUSPENDED)) &
              Q(application__end_date__lt=datetime.now()))
         return self.filter(q)
 
-    def search_by_name(self, *search_strings):
-        q = Q()
-        for s in search_strings:
-            q = q | Q(name__icontains=s)
-        return self.filter(q)
-
 
 class Project(models.Model):
 
-    id = models.OneToOneField(Chain,
-                              related_name='chained_project',
-                              db_column='id',
-                              primary_key=True)
+    id = models.BigIntegerField(db_column='id', primary_key=True)
 
     application = models.OneToOneField(
         ProjectApplication,
@@ -1817,20 +1675,15 @@ class Project(models.Model):
         db_index=True,
         unique=True)
 
-    APPROVED = 1
+    NORMAL = 1
     SUSPENDED = 10
     TERMINATED = 100
 
-    state = models.IntegerField(default=APPROVED,
+    state = models.IntegerField(default=NORMAL,
                                 db_index=True)
 
     objects = ProjectManager()
 
-    # Compiled queries
-    Q_TERMINATED = Q(state=TERMINATED)
-    Q_SUSPENDED = Q(state=SUSPENDED)
-    Q_DEACTIVATED = Q_TERMINATED | Q_SUSPENDED
-
     def __str__(self):
         return uenc(_("<project %s '%s'>") %
                     (self.id, udec(self.application.name)))
@@ -1840,14 +1693,74 @@ class Project(models.Model):
     def __unicode__(self):
         return _("<project %s '%s'>") % (self.id, self.application.name)
 
-    STATE_DISPLAY = {
-        APPROVED:   'Active',
-        SUSPENDED:  'Suspended',
-        TERMINATED: 'Terminated'
+    O_PENDING = 0
+    O_ACTIVE = 1
+    O_DENIED = 3
+    O_DISMISSED = 4
+    O_CANCELLED = 5
+    O_SUSPENDED = 10
+    O_TERMINATED = 100
+
+    O_STATE_DISPLAY = {
+        O_PENDING:    _("Pending"),
+        O_ACTIVE:     _("Active"),
+        O_DENIED:     _("Denied"),
+        O_DISMISSED:  _("Dismissed"),
+        O_CANCELLED:  _("Cancelled"),
+        O_SUSPENDED:  _("Suspended"),
+        O_TERMINATED: _("Terminated"),
     }
 
+    OVERALL_STATE = {
+        (NORMAL, ProjectApplication.PENDING):      O_PENDING,
+        (NORMAL, ProjectApplication.APPROVED):     O_ACTIVE,
+        (NORMAL, ProjectApplication.DENIED):       O_DENIED,
+        (NORMAL, ProjectApplication.DISMISSED):    O_DISMISSED,
+        (NORMAL, ProjectApplication.CANCELLED):    O_CANCELLED,
+        (SUSPENDED, ProjectApplication.APPROVED):  O_SUSPENDED,
+        (TERMINATED, ProjectApplication.APPROVED): O_TERMINATED,
+    }
+
+    OVERALL_STATE_INV = invert_dict(OVERALL_STATE)
+
+    @classmethod
+    def o_state_q(cls, o_state):
+        p_state, a_state = cls.OVERALL_STATE_INV[o_state]
+        return Q(state=p_state, application__state=a_state)
+
+    INITIALIZED_STATES = [O_ACTIVE,
+                          O_SUSPENDED,
+                          O_TERMINATED,
+                          ]
+
+    RELEVANT_STATES = [O_PENDING,
+                       O_DENIED,
+                       O_ACTIVE,
+                       O_SUSPENDED,
+                       O_TERMINATED,
+                       ]
+
+    SKIP_STATES = [O_DISMISSED,
+                   O_CANCELLED,
+                   O_TERMINATED,
+                   ]
+
+    @classmethod
+    def _overall_state(cls, project_state, app_state):
+        return cls.OVERALL_STATE.get((project_state, app_state), None)
+
+    def overall_state(self):
+        return self._overall_state(self.state, self.application.state)
+
+    def last_pending_application(self):
+        apps = self.chained_apps.filter(
+            state=ProjectApplication.PENDING).order_by('-id')
+        if apps:
+            return apps[0]
+        return None
+
     def state_display(self):
-        return self.STATE_DISPLAY.get(self.state, _('Unknown'))
+        return self.O_STATE_DISPLAY.get(self.overall_state(), _('Unknown'))
 
     def expiration_info(self):
         return (str(self.id), self.name, self.state_display(),
@@ -1857,7 +1770,13 @@ class Project(models.Model):
         if reason is not None:
             return self.state == reason
 
-        return self.state != self.APPROVED
+        return self.state != self.NORMAL
+
+    def is_active(self):
+        return self.overall_state() == self.O_ACTIVE
+
+    def is_initialized(self):
+        return self.overall_state() in self.INITIALIZED_STATES
 
     ### Deactivation calls
 
@@ -1877,7 +1796,7 @@ class Project(models.Model):
     def resume(self):
         self.deactivation_reason = None
         self.deactivation_date = None
-        self.state = self.APPROVED
+        self.state = self.NORMAL
         self.save()
 
     ### Logical checks
@@ -1889,12 +1808,9 @@ class Project(models.Model):
                  self.deactivation_date]
         return any([date > now for date in dates])
 
-    def is_approved(self):
-        return self.state == self.APPROVED
-
     @property
     def is_alive(self):
-        return not self.is_terminated
+        return self.overall_state() in [self.O_ACTIVE, self.O_SUSPENDED]
 
     @property
     def is_terminated(self):
@@ -1904,9 +1820,6 @@ class Project(models.Model):
     def is_suspended(self):
         return self.is_deactivated(self.SUSPENDED)
 
-    def violates_resource_grants(self):
-        return False
-
     def violates_members_limit(self, adding=0):
         application = self.application
         limit = application.limit_on_members_number
@@ -1932,32 +1845,6 @@ class Project(models.Model):
         return [m.person for m in self.approved_memberships]
 
 
-CHAIN_STATE = {
-    (Project.APPROVED, ProjectApplication.PENDING):   Chain.APPROVED_PENDING,
-    (Project.APPROVED, ProjectApplication.APPROVED):  Chain.APPROVED,
-    (Project.APPROVED, ProjectApplication.DENIED):    Chain.APPROVED,
-    (Project.APPROVED, ProjectApplication.DISMISSED): Chain.APPROVED,
-    (Project.APPROVED, ProjectApplication.CANCELLED): Chain.APPROVED,
-
-    (Project.SUSPENDED, ProjectApplication.PENDING):   Chain.SUSPENDED_PENDING,
-    (Project.SUSPENDED, ProjectApplication.APPROVED):  Chain.SUSPENDED,
-    (Project.SUSPENDED, ProjectApplication.DENIED):    Chain.SUSPENDED,
-    (Project.SUSPENDED, ProjectApplication.DISMISSED): Chain.SUSPENDED,
-    (Project.SUSPENDED, ProjectApplication.CANCELLED): Chain.SUSPENDED,
-
-    (Project.TERMINATED, ProjectApplication.PENDING): Chain.TERMINATED_PENDING,
-    (Project.TERMINATED, ProjectApplication.APPROVED):  Chain.TERMINATED,
-    (Project.TERMINATED, ProjectApplication.DENIED):    Chain.TERMINATED,
-    (Project.TERMINATED, ProjectApplication.DISMISSED): Chain.TERMINATED,
-    (Project.TERMINATED, ProjectApplication.CANCELLED): Chain.TERMINATED,
-
-    (None, ProjectApplication.PENDING):   Chain.PENDING,
-    (None, ProjectApplication.DENIED):    Chain.DENIED,
-    (None, ProjectApplication.DISMISSED): Chain.DISMISSED,
-    (None, ProjectApplication.CANCELLED): Chain.CANCELLED,
-}
-
-
 class ProjectMembershipManager(ForUpdateManager):
 
     def any_accepted(self):
