Commit cd8ddfcc authored by Giorgos Korfiatis's avatar Giorgos Korfiatis
Browse files

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