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
This diff is collapsed.
......@@ -209,18 +209,15 @@ def astakos_users_quotas(users, initial=None):
quotas = copy.deepcopy(initial)
ACTUALLY_ACCEPTED = ProjectMembership.ACTUALLY_ACCEPTED
objs = ProjectMembership.objects.select_related('project', 'person')
memberships = objs.filter(person__in=users,
state__in=ACTUALLY_ACCEPTED,
project__state=Project.APPROVED)
objs = ProjectMembership.objects.select_related(
'project', 'person', 'project__application')
memberships = objs.filter(
person__in=users,
state__in=ACTUALLY_ACCEPTED,
project__state=Project.NORMAL,
project__application__state=ProjectApplication.APPROVED)
project_ids = set(m.project_id for m in memberships)
objs = ProjectApplication.objects.select_related('project')
apps = objs.filter(project__in=project_ids)
project_dict = {}
for app in apps:
project_dict[app.project] = app
apps = set(m.project.application_id for m in memberships)
objs = ProjectResourceGrant.objects.select_related()
grants = objs.filter(project_application__in=apps)
......@@ -229,7 +226,7 @@ def astakos_users_quotas(users, initial=None):
uuid = membership.person.uuid
userquotas = quotas.get(uuid, {})
application = project_dict[membership.project]
application = membership.project.application
for grant in grants:
if grant.project_application_id != application.id:
......
......@@ -185,9 +185,9 @@ def action_extra_context(application, table, self):
append_url = ''
can_join = can_leave = can_cancel = False
project = application.get_project()
project = application.chain
if project and project.is_approved():
if project.is_active():
try:
join_project_checks(project)
can_join = True
......@@ -228,7 +228,7 @@ def action_extra_context(application, table, self):
confirm = False
url = None
url = reverse(url, args=(application.chain, )) + append_url if url else ''
url = reverse(url, args=(application.chain_id, )) + append_url if url else ''
return {'action': action,
'confirm': confirm,
......@@ -264,7 +264,7 @@ class UserProjectApplicationsTable(UserTable):
name = LinkColumn('astakos.im.views.project_detail',
coerce=lambda x: truncatename(x, 25),
append=project_name_append,
args=(A('chain'),))
args=(A('chain_id'),))
issue_date = tables.DateColumn(verbose_name=_('Application'),
format=DEFAULT_DATE_FORMAT)
start_date = tables.DateColumn(format=DEFAULT_DATE_FORMAT)
......@@ -292,7 +292,7 @@ class UserProjectApplicationsTable(UserTable):
def render_members_count(self, record, *args, **kwargs):
append = ""
application = record
project = application.get_project()
project = application.chain
if project is None:
append = mark_safe("<i class='tiny'>%s</i>" % (_('pending'),))
......@@ -300,7 +300,7 @@ class UserProjectApplicationsTable(UserTable):
if c > 0:
pending_members_url = reverse(
'project_pending_members',
kwargs={'chain_id': application.chain})
kwargs={'chain_id': application.chain_id})
pending_members = "<i class='tiny'> - %d %s</i>" % (
c, _('pending'))
......@@ -313,7 +313,7 @@ class UserProjectApplicationsTable(UserTable):
(pending_members_url, c, _('pending')))
append = mark_safe(pending_members)
members_url = reverse('project_approved_members',
kwargs={'chain_id': application.chain})
kwargs={'chain_id': application.chain_id})
members_count = record.members_count()
if self.user.owns_application(record) or self.user.is_project_admin():
members_count = '<a href="%s">%d</a>' % (members_url,
......
......@@ -12,7 +12,7 @@
{% if owner_mode %}
{% with object.last_pending_incl_me as last_pending %}
{% if last_pending %}
{% if object.project_exists %}
{% if object.project.is_initialized %}
- {% confirm_link "CANCEL PROJECT MODIFICATION" "project_modification_cancel" "project_app_cancel" last_pending.pk "" "OK" %}
{% else %}
- {% confirm_link "CANCEL PROJECT APPLICATION" "project_app_cancel" "project_app_cancel" last_pending.pk "" "OK" %}
......
<form action="{% url project_detail object.chain %}#members-table"
<form action="{% url project_detail object.chain_id %}#members-table"
method="post" class="withlabels upperlabels" id="members-table" >
{% csrf_token %}
{% with addmembers_form as form %}
......@@ -7,4 +7,4 @@
<div class="form-row submit">
<input type="submit" class="submit altcol" value="ADD MEMBERS" />
</div>
</form>
\ No newline at end of file
</form>
......@@ -2,10 +2,10 @@
{% load filters %}
{% block content %}
The following project has been created:
The following project application has been submitted:
Id: {{object.id}}
Project: {{object.chain_id}}
Name: {{object.name}}
Issue date: {{object.issue_date|date:"d/m/Y"}}
Start date: {{object.start_date|date:"d/m/Y"}}
......@@ -22,5 +22,5 @@ Policies:
{% endfor %}
For approving it you can use the command line tool:
snf-manage project-control --approve <id>
{% endblock content %}
\ No newline at end of file
snf-manage project-control --approve {{object.id}}
{% endblock content %}
......@@ -119,7 +119,7 @@
<h3>
{% if owner_mode and project_view %}
{% if object.project.is_alive %}
<a href="{% url project_members object.chain %}">MEMBERS </a>
<a href="{% url project_members object.chain_id %}">MEMBERS </a>
{% else %}
MEMBERS
{% endif %}
......@@ -147,7 +147,7 @@
</dd>
{% if owner_mode and project_view %}
{% if object.project.is_alive %}
<dt><a href="{% url project_approved_members object.chain %}" title="view approved members">Approved members</a></dt>
<dt><a href="{% url project_approved_members object.chain_id %}" title="view approved members">Approved members</a></dt>
<dd>{{ approved_members_count }}
<span class="faint">
{% if remaining_memberships_count != None %}
......@@ -157,7 +157,7 @@
{% else %}&nbsp;{% endif %}
</span>
</dd>
<dt><a href="{% url project_pending_members object.chain %}" title="view pending members">Members pending approval</a></dt>
<dt><a href="{% url project_pending_members object.chain_id %}" title="view pending members">Members pending approval</a></dt>
<dd>{{ pending_members_count }}</dd>
{% if not project.is_deactivated %}
</dl>
......