Commit cd8ddfcc authored by Giorgos Korfiatis's avatar Giorgos Korfiatis

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.
parent 9a0200b5
......@@ -16,6 +16,16 @@ Synnefo-wide
* Integrate Pithos tests in continuous integration.
Astakos
-------
* Changes in project schema:
* A Project entry is created when submitting an application for a new
project, rather than on approval. Its state is dependent on the state
of its `reference' application (current definition).
Cyclades
--------
......
......@@ -32,6 +32,7 @@
# or implied, of GRNET S.A.
import logging
from datetime import datetime
from django.utils.translation import ugettext as _
from django.core.mail import send_mail, get_connection
......@@ -39,7 +40,6 @@ from django.core.urlresolvers import reverse
from django.contrib.auth import login as auth_login, logout as auth_logout
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.http import Http404
from synnefo_branding.utils import render_to_string
......@@ -251,30 +251,10 @@ CLOSED_POLICY = 3
POLICIES = [AUTO_ACCEPT_POLICY, MODERATED_POLICY, CLOSED_POLICY]
def get_project_by_application_id(project_application_id):
try:
return Project.objects.get(application__id=project_application_id)
except Project.DoesNotExist:
m = (_(astakos_messages.UNKNOWN_PROJECT_APPLICATION_ID) %
project_application_id)
raise IOError(m)
def get_related_project_id(application_id):
try:
app = ProjectApplication.objects.get(id=application_id)
chain = app.chain
Project.objects.get(id=chain)
return chain
except (ProjectApplication.DoesNotExist, Project.DoesNotExist):
return None
def get_chain_of_application_id(application_id):
try:
app = ProjectApplication.objects.get(id=application_id)
chain = app.chain
return chain.chain
return app.chain_id
except ProjectApplication.DoesNotExist:
return None
......@@ -287,14 +267,6 @@ def get_project_by_id(project_id):
raise IOError(m)
def get_project_by_name(name):
try:
return Project.objects.get(name=name)
except Project.DoesNotExist:
m = _(astakos_messages.UNKNOWN_PROJECT_ID) % name
raise IOError(m)
def get_chain_for_update(chain_id):
try:
return Chain.objects.get_for_update(chain=chain_id)
......@@ -646,17 +618,25 @@ def submit_application(owner=None,
comments=comments)
if precursor is None:
application.chain = new_chain()
chain = new_chain()
application.chain_id = chain.chain
application.save()
Project.objects.create(id=chain.chain, application=application)
else:
chain = precursor.chain
application.chain = chain
objs = ProjectApplication.objects
pending = objs.filter(chain=chain, state=ProjectApplication.PENDING)
application.save()
if chain.application.state != ProjectApplication.APPROVED:
chain.application = application
chain.save()
pending = ProjectApplication.objects.filter(
chain=chain,
state=ProjectApplication.PENDING).exclude(id=application.id)
for app in pending:
app.state = ProjectApplication.REPLACED
app.save()
application.save()
if resource_policies is not None:
application.set_resource_policies(resource_policies)
logger.info("User %s submitted %s." %
......@@ -715,11 +695,7 @@ def deny_application(application_id, request_user=None, reason=""):
def check_conflicting_projects(application):
try:
project = get_project_by_id(application.chain)
except IOError:
project = None
project = application.chain
new_project_name = application.name
try:
q = Q(name=new_project_name) & ~Q(state=Project.TERMINATED)
......@@ -732,12 +708,11 @@ def check_conflicting_projects(application):
except Project.DoesNotExist:
pass
return project
def approve_application(app_id, request_user=None, reason=""):
get_chain_of_application_for_update(app_id)
application = get_application(app_id)
project = application.chain
checkAllowed(application, request_user, admin_only=True)
......@@ -746,11 +721,11 @@ def approve_application(app_id, request_user=None, reason=""):
(application.id, application.state_display()))
raise PermissionDenied(m)
project = check_conflicting_projects(application)
check_conflicting_projects(application)
# Pre-lock members and owner together in order to impose an ordering
# on locking users
members = members_to_sync(project) if project is not None else []
members = members_to_sync(project)
uids_to_sync = [member.id for member in members]
owner = application.owner
uids_to_sync.append(owner.id)
......@@ -758,6 +733,11 @@ def approve_application(app_id, request_user=None, reason=""):
qh_release_pending_app(owner, locked=True)
application.approve(reason)
project.application = application
project.name = application.name
project.save()
if project.is_deactivated():
project.resume()
qh_sync_locked_users(members)
logger.info("%s has been approved." % (application.log_display))
application_approve_notify(application)
......@@ -813,19 +793,6 @@ def resume(project_id, request_user=None):
logger.info("%s has been unsuspended." % (project))
def get_by_chain_or_404(chain_id):
try:
project = Project.objects.get(id=chain_id)
application = project.application
return project, application
except:
application = ProjectApplication.objects.latest_of_chain(chain_id)
if application is None:
raise Http404
else:
return None, application
def _partition_by(f, l):
d = {}
for x in l:
......
......@@ -35,7 +35,8 @@ from optparse import make_option
from snf_django.management.commands import SynnefoCommand, CommandError
from astakos.im.models import Chain
from astakos.im.models import Project
from django.db.models import Q
from snf_django.management import utils
from ._common import is_uuid, is_email
......@@ -107,86 +108,74 @@ class Command(SynnefoCommand):
def handle(self, *args, **options):
chain_dict = Chain.objects.all_full_state()
flt = Q()
owner = options['owner']
if owner:
flt &= filter_by_owner(owner)
if not options['all']:
f_states = []
if options['new']:
f_states.append(Chain.PENDING)
if options['modified']:
f_states += Chain.MODIFICATION_STATES
if options['pending']:
f_states.append(Chain.PENDING)
f_states += Chain.MODIFICATION_STATES
if options['skip']:
if not f_states:
f_states = Chain.RELEVANT_STATES
if f_states:
chain_dict = filter_by(in_states(f_states), chain_dict)
name = options['name']
if name:
chain_dict = filter_by(is_name(name), chain_dict)
owner = options['owner']
if owner:
chain_dict = filter_by(is_owner(owner), chain_dict)
name = options['name']
if name:
flt &= filter_by_name(name)
labels = ('ProjID', 'Name', 'Owner', 'Email', 'Status', 'AppID')
chains = Project.objects.all_with_pending(flt)
info = chain_info(chain_dict)
if not options['all']:
if options['skip']:
pred = lambda c: (
c[0].overall_state() not in Project.SKIP_STATES
or c[1] is not None)
chains = filter_preds([pred], chains)
preds = []
if options['new'] or options['pending']:
preds.append(
lambda c: c[0].overall_state() == Project.O_PENDING)
if options['modified'] or options['pending']:
preds.append(
lambda c: c[0].overall_state() != Project.O_PENDING
and c[1] is not None)
if preds:
chains = filter_preds(preds, chains)
labels = ('ProjID', 'Name', 'Owner', 'Email', 'Status',
'Pending AppID')
info = chain_info(chains)
utils.pprint_table(self.stdout, info, labels,
options["output_format"])
def is_name(name):
def f(state, project, app):
n = project.application.name if project else app.name
return name == n
return f
def in_states(states):
def f(state, project, app):
return state in states
return f
def filter_preds(preds, chains):
return [c for c in chains
if any(map(lambda f: f(c), preds))]
def is_owner(s):
def f(state, project, app):
owner = app.owner
if is_email(s):
return owner.email == s
if is_uuid(s):
return owner.uuid == s
raise CommandError("Expecting either email or uuid.")
return f
def filter_by_name(name):
return Q(application__name=name)
def filter_by(f, chain_dict):
d = {}
for chain, tpl in chain_dict.iteritems():
if f(*tpl):
d[chain] = tpl
return d
def filter_by_owner(s):
if is_email(s):
return Q(application__owner__email=s)
if is_uuid(s):
return Q(application__owner__uuid=s)
raise CommandError("Expecting either email or uuid.")
def chain_info(chain_dict):
def chain_info(chains):
l = []
for chain, (state, project, app) in chain_dict.iteritems():
status = Chain.state_display(state)
if state in Chain.PENDING_STATES:
appid = str(app.id)
else:
appid = ""
t = (chain,
project.application.name if project else app.name,
app.owner.realname,
app.owner.email,
for project, pending_app in chains:
status = project.state_display()
pending_appid = pending_app.id if pending_app is not None else ""
application = project.application
t = (project.pk,
application.name,
application.owner.realname,
application.owner.email,
status,
appid,
pending_appid,
)
l.append(t)
return l
......@@ -37,7 +37,7 @@ from django.core.management.base import CommandError
from synnefo.lib.ordereddict import OrderedDict
from snf_django.management.commands import SynnefoCommand
from snf_django.management import utils
from astakos.im.models import Chain, ProjectApplication
from astakos.im.models import ProjectApplication, Project
from ._common import show_resource_value, style_options, check_style
......@@ -93,15 +93,15 @@ class Command(SynnefoCommand):
app = get_app(id_)
self.print_app(app)
else:
state, project, app = get_chain_state(id_)
self.print_project(state, project, app)
project, pending_app = get_chain_state(id_)
self.print_project(project, pending_app)
if show_members and project is not None:
self.stdout.write("\n")
fields, labels = members_fields(project)
self.pprint_table(fields, labels, title="Members")
if show_pending and state in Chain.PENDING_STATES:
if show_pending and pending_app is not None:
self.stdout.write("\n")
self.print_app(app)
self.print_app(pending_app)
def pprint_dict(self, d, vertical=True):
utils.pprint_table(self.stdout, [d.values()], d.keys(),
......@@ -116,11 +116,11 @@ class Command(SynnefoCommand):
self.pprint_dict(app_info)
self.print_resources(app)
def print_project(self, state, project, app):
def print_project(self, project, app):
if project is None:
self.print_app(app)
else:
self.pprint_dict(project_fields(state, project, app))
self.pprint_dict(project_fields(project, app))
self.print_resources(project.application)
def print_resources(self, app):
......@@ -139,19 +139,12 @@ def get_app(app_id):
def get_chain_state(project_id):
try:
chain = Chain.objects.get(chain=project_id)
return chain.full_state()
except Chain.DoesNotExist:
chain = Project.objects.get(id=project_id)
return chain, chain.last_pending_application()
except Project.DoesNotExist:
raise CommandError("Project with id %s not found." % project_id)
def chain_fields(state, project, app):
if project is not None:
return project_fields(state, project, app)
else:
return app_fields(app)
def resource_fields(app, style):
labels = ('name', 'description', 'max per member')
policies = app.projectresourcegrant_set.all()
......@@ -170,7 +163,7 @@ def app_fields(app):
mem_limit_show = mem_limit if mem_limit is not None else "unlimited"
d = OrderedDict([
('project id', app.chain),
('project id', app.chain_id),
('application id', app.id),
('name', app.name),
('status', app.state_display()),
......@@ -190,17 +183,17 @@ def app_fields(app):
return d
def project_fields(s, project, last_app):
def project_fields(project, pending_app):
app = project.application
d = OrderedDict([
('project id', project.id),
('application id', app.id),
('name', app.name),
('status', Chain.state_display(s)),
('status', project.state_display()),
])
if s in Chain.PENDING_STATES:
d.update([('pending application', last_app.id)])
if pending_app is not None:
d.update([('pending application', pending_app.id)])
d.update([('owner', app.owner),
('applicant', app.applicant),
......
......@@ -34,7 +34,8 @@
from django.core.management.base import CommandError
from optparse import make_option
from astakos.im.models import AstakosUser, get_latest_terms, Chain
from django.db.models import Q
from astakos.im.models import AstakosUser, get_latest_terms, Project
from astakos.im.quotas import list_user_quotas
from synnefo.lib.ordereddict import OrderedDict
......@@ -166,39 +167,22 @@ def memberships(user):
def ownerships(user):
chain_dict = Chain.objects.all_full_state()
chain_dict = filter_by(is_owner(user), chain_dict)
return chain_info(chain_dict)
chains = Project.objects.all_with_pending(Q(application__owner=user))
return chain_info(chains)
def is_owner(user):
def f(state, project, app):
return user == app.owner
return f
def filter_by(f, chain_dict):
d = {}
for chain, tpl in chain_dict.iteritems():
if f(*tpl):
d[chain] = tpl
return d
def chain_info(chain_dict):
def chain_info(chains):
labels = ('project id', 'project name', 'status', 'pending app id')
l = []
for chain, (state, project, app) in chain_dict.iteritems():
status = Chain.state_display(state)
if state in Chain.PENDING_STATES:
appid = str(app.id)
else:
appid = ""
for project, pending_app in chains:
status = project.state_display()
pending_appid = pending_app.id if pending_app is not None else ""
application = project.application
t = (chain,
project.application.name if project else app.name,
t = (project.pk,
application.name,
status,
appid,
pending_appid,
)
l.append(t)
return l, labels
......@@ -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"),
}