commit 7eadc230fe2a1617ac7d7449a632e830b70b32ae
Author: Giorgos Korfiatis <gkorf@grnet.gr>
Date:   Thu Jan 17 18:52:10 2013 +0200

    Management command to check and perform project expiration

diff --git a/snf-astakos-app/astakos/im/functions.py b/snf-astakos-app/astakos/im/functions.py
index 404be58..7df7f1b 100644
--- a/snf-astakos-app/astakos/im/functions.py
+++ b/snf-astakos-app/astakos/im/functions.py
@@ -702,6 +702,15 @@ def approve_application(app):
 
     application_approve_notify(application)
 
+def check_expiration(execute=False):
+    objects = Project.objects
+    expired = objects.expired_projects()
+    if execute:
+        for project in expired:
+            terminate(project.id)
+
+    return [project.expiration_info() for project in expired]
+
 def terminate(project_id):
     project = get_project_for_update(project_id)
     checkAlive(project)
diff --git a/snf-astakos-app/astakos/im/management/commands/project-admin-checks.py b/snf-astakos-app/astakos/im/management/commands/project-admin-checks.py
new file mode 100644
index 0000000..6ec13f4
--- /dev/null
+++ b/snf-astakos-app/astakos/im/management/commands/project-admin-checks.py
@@ -0,0 +1,97 @@
+# 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.db import transaction
+
+from astakos.im.functions import check_expiration
+
+@transaction.commit_manually
+class Command(BaseCommand):
+    args = "<project id>"
+    help = "Update project state"
+
+    option_list = BaseCommand.option_list + (
+        make_option('--expire',
+                    action='store_true',
+                    dest='expire',
+                    default=False,
+                    help="Check projects for expiration"),
+        make_option('--execute',
+                    action='store_true',
+                    dest='execute',
+                    default=False,
+                    help="Perform the actual operation"),
+    )
+
+
+    def print_expired(self, projects, execute):
+        length = len(projects)
+        if length == 0:
+            s = 'No expired projects.\n'
+        elif length == 1:
+            s = '1 expired project:\n'
+        else:
+            s = '%d expired projects:\n' %(length,)
+        self.stdout.write(s)
+
+        if length > 0:
+            labels = ('Project', 'Name', 'Status', 'Expiration date')
+            columns = (10, 30, 14, 30)
+
+            line = ' '.join(l.rjust(w) for l, w in zip(labels, columns))
+            self.stdout.write(line + '\n')
+            sep = '-' * len(line)
+            self.stdout.write(sep + '\n')
+
+            for project in projects:
+                line = ' '.join(f.rjust(w) for f, w in zip(project, columns))
+                self.stdout.write(line.encode('utf8') + '\n')
+
+            if execute:
+                self.stdout.write('%d projects have been terminated.\n' %(length,))
+
+    def handle(self, *args, **options):
+
+        execute = options['execute']
+
+        try:
+            if options['expire']:
+                projects = check_expiration(execute=execute)
+                self.print_expired(projects, execute)
+        except BaseException as e:
+            transaction.rollback()
+            raise CommandError(e)
+        else:
+            transaction.commit()
diff --git a/snf-astakos-app/astakos/im/models.py b/snf-astakos-app/astakos/im/models.py
index 77fa716..b9c9f8f 100644
--- a/snf-astakos-app/astakos/im/models.py
+++ b/snf-astakos-app/astakos/im/models.py
@@ -1556,6 +1556,12 @@ class ProjectManager(ForUpdateManager):
     def reactivating_projects(self):
         return self.filter(state=Project.APPROVED, is_active=False)
 
+    def expired_projects(self):
+        q = (~Q(state=Project.TERMINATED) &
+              Q(application__end_date__lt=datetime.now()))
+        return self.filter(q)
+
+
 class Project(models.Model):
 
     application                 =   models.OneToOneField(
@@ -1608,6 +1614,10 @@ class Project(models.Model):
     def state_display(self):
         return self.STATE_DISPLAY.get(self.state, _('Unknown'))
 
+    def expiration_info(self):
+        return (str(self.id), self.name, self.state_display(),
+                str(self.application.end_date))
+
     def is_deactivated(self, reason=None):
         if reason is not None:
             return self.state == reason
