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

Handle max pending applications as a quotable resource

Assume a resource `astakos.pending_app' related to service `astakos'.
On submitting an application, issue a commission that will update the
related counter if possible, and accept the commission on success.
On approving/denying/cancelling an application, update the counter
likewise.

We always charge the application `owner'; however, if the `applicant'
is a project admin, we issue commission in force mode which succeeds
even if it exceeds the limit.

In order to pre-emptively check in the UI, follow the same process in
`dry run' mode, which issues a commission and rejects it on success.

Remove option of user-update to set its limit.

Refs #3349
parent bfdb91cf
......@@ -175,15 +175,17 @@ If you want to enable the projects feature so that users may apply
on their own for resources by creating and joining projects,
in ``20-snf-astakos-app-settings.conf`` set::
# this will allow at most one pending project application per user
ASTAKOS_PENDING_APPLICATION_LIMIT = 1
# this will make the 'projects' page visible in the dashboard
ASTAKOS_PROJECTS_VISIBLE = True
You can specify a user-specific limit on pending project applications
with::
You can change the maximum allowed number of pending project applications
per user with::
# snf-manage user-update <user id> --max-pending-projects=2
# snf-manage resource-modify astakos.pending_app --limit <number>
You can also set a user-specific limit with::
# snf-manage user-set-initial-quota --set-capacity 'user-uuid' 'astakos.pending_app' 5
When users apply for projects they are not automatically granted
the resources. They must first be approved by the administrator.
......
......@@ -71,7 +71,8 @@ from astakos.im.models import (
AstakosUser, Invitation, ProjectMembership, ProjectApplication, Project,
UserSetting,
get_resource_names, new_chain)
from astakos.im.quotas import qh_sync_user, qh_sync_users
from astakos.im.quotas import (qh_sync_user, qh_sync_users,
register_pending_apps, resolve_pending_serial)
from astakos.im.project_notif import (
membership_change_notify, membership_enroll_notify,
membership_request_notify, membership_leave_request_notify,
......@@ -80,6 +81,7 @@ from astakos.im.project_notif import (
project_termination_notify, project_suspension_notify)
import astakos.im.messages as astakos_messages
from astakos.quotaholder.exception import NoCapacityError
logger = logging.getLogger(__name__)
......@@ -727,8 +729,9 @@ def submit_application(kw, request_user=None):
raise PermissionDenied(m)
owner = kw['owner']
reached, limit = reached_pending_application_limit(owner.id, precursor)
if not request_user.is_project_admin() and reached:
force = request_user.is_project_admin()
ok, limit = qh_add_pending_app(owner, precursor, force)
if not ok:
m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
raise PermissionDenied(m)
......@@ -763,6 +766,8 @@ def cancel_application(application_id, request_user=None):
(application.id, application.state_display()))
raise PermissionDenied(m)
qh_release_pending_app(application.owner)
application.cancel()
logger.info("%s has been cancelled." % (application.log_display))
......@@ -790,6 +795,8 @@ def deny_application(application_id, request_user=None, reason=None):
(application.id, application.state_display()))
raise PermissionDenied(m)
qh_release_pending_app(application.owner)
if reason is None:
reason = ""
application.deny(reason)
......@@ -814,6 +821,7 @@ def approve_application(app_id, request_user=None):
(application.id, application.state_display()))
raise PermissionDenied(m)
qh_release_pending_app(application.owner)
project = application.approve()
qh_sync_projects([project])
logger.info("%s has been approved." % (application.log_display))
......@@ -903,49 +911,31 @@ def unset_user_setting(user_id, key):
UserSetting.objects.filter(user=user_id, setting=key).delete()
PENDING_APPLICATION_LIMIT_SETTING = 'PENDING_APPLICATION_LIMIT'
def get_pending_application_limit(user_id):
key = PENDING_APPLICATION_LIMIT_SETTING
return get_user_setting(user_id, key)
def set_pending_application_limit(user_id, value):
key = PENDING_APPLICATION_LIMIT_SETTING
return set_user_setting(user_id, key, value)
def unset_pending_application_limit(user_id):
key = PENDING_APPLICATION_LIMIT_SETTING
return unset_user_setting(user_id, key)
def _reached_pending_application_limit(user_id):
limit = get_pending_application_limit(user_id)
PENDING = ProjectApplication.PENDING
pending = ProjectApplication.objects.filter(
owner__id=user_id, state=PENDING).count()
return pending >= limit, limit
def reached_pending_application_limit(user_id, precursor=None):
reached, limit = _reached_pending_application_limit(user_id)
def qh_add_pending_app(user, precursor=None, force=False, dry_run=False):
if precursor is None:
return reached, limit
diff = 1
else:
chain = precursor.chain
objs = ProjectApplication.objects
q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
count = q.count()
diff = 1 - count
chain = precursor.chain
objs = ProjectApplication.objects
q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
has_pending = q.exists()
try:
name = "DRYRUN" if dry_run else ""
serial = register_pending_apps(user, diff, force, name=name)
except NoCapacityError as e:
limit = e.data['limit']
return False, limit
else:
accept = not dry_run
resolve_pending_serial(serial, accept=accept)
return True, None
if not has_pending:
return reached, limit
return False, limit
def qh_release_pending_app(user):
serial = register_pending_apps(user, -1)
resolve_pending_serial(serial)
def qh_sync_projects(projects):
......
......@@ -40,9 +40,7 @@ from django.contrib.auth.models import Group
from django.core.exceptions import ValidationError
from astakos.im.models import AstakosUser
from astakos.im.functions import (activate, deactivate,
set_pending_application_limit,
unset_pending_application_limit)
from astakos.im.functions import (activate, deactivate)
from ._common import remove_user_permission, add_user_permission
from snf_django.lib.db.transaction import commit_on_success_strict
......@@ -106,17 +104,6 @@ class Command(BaseCommand):
make_option('--delete-permission',
dest='delete-permission',
help="Delete user permission"),
make_option('--max-pending-projects',
dest='pending',
metavar='INT',
help=("Set limit on user's maximum pending "
"project applications")),
make_option('--reset-max-pending-projects',
dest='unset_pending',
action='store_true',
default=False,
help=("Restore default limit of user's maximum pending "
"project applications")),
)
@commit_on_success_strict()
......@@ -219,17 +206,3 @@ class Command(BaseCommand):
if password:
self.stdout.write('User\'s new password: %s\n' % password)
pending = options.get('pending')
if pending:
try:
pending = int(pending)
except ValueError as e:
m = _("Expected integer argument")
raise CommandError(m)
else:
set_pending_application_limit(user_id, pending)
unset_pending = options.get('unset_pending')
if unset_pending:
unset_pending_application_limit(user_id)
......@@ -129,6 +129,20 @@ def get_default_quota():
SYSTEM = 'system'
def resolve_pending_serial(serial, accept=True):
return qh.resolve_pending_commission('astakos', serial, accept)
def register_pending_apps(user, quantity, force=False, name=None):
provision = (user.uuid, SYSTEM, 'astakos.pending_app'), quantity
s = qh.issue_commission(clientkey='astakos',
force=force,
name=name,
provisions=[provision])
return s
def initial_quotas(users):
initial = {}
default_quotas = get_default_quota()
......
......@@ -97,7 +97,7 @@ from astakos.im.functions import (
invite as invite_func,
send_activation as send_activation_func,
SendNotificationError,
reached_pending_application_limit,
qh_add_pending_app,
accept_membership, reject_membership, remove_membership, cancel_membership,
leave_project, join_project, enroll_member, can_join_request, can_leave_request,
get_related_project_id, get_by_chain_or_404,
......@@ -1085,13 +1085,14 @@ def _resources_catalog(request):
@valid_astakos_user_required
def project_add(request):
user = request.user
reached, limit = reached_pending_application_limit(user.id)
if not user.is_project_admin() and reached:
m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit
messages.error(request, m)
next = reverse('astakos.im.views.project_list')
next = restrict_next(next, domain=COOKIE_DOMAIN)
return redirect(next)
if not user.is_project_admin():
ok, limit = qh_add_pending_app(user, dry_run=True)
if not ok:
m = _(astakos_messages.PENDING_APPLICATION_LIMIT_ADD) % limit
messages.error(request, m)
next = reverse('astakos.im.views.project_list')
next = restrict_next(next, domain=COOKIE_DOMAIN)
return redirect(next)
details_fields = ["name", "homepage", "description", "start_date",
"end_date", "comments"]
......@@ -1190,14 +1191,15 @@ def project_modify(request, application_id):
m = _(astakos_messages.NOT_ALLOWED)
raise PermissionDenied(m)
owner_id = app.owner_id
reached, limit = reached_pending_application_limit(owner_id, app)
if not user.is_project_admin() and reached:
m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
messages.error(request, m)
next = reverse('astakos.im.views.project_list')
next = restrict_next(next, domain=COOKIE_DOMAIN)
return redirect(next)
if not user.is_project_admin():
owner = app.owner
ok, limit = qh_add_pending_app(owner, precursor=app, dry_run=True)
if not ok:
m = _(astakos_messages.PENDING_APPLICATION_LIMIT_MODIFY) % limit
messages.error(request, m)
next = reverse('astakos.im.views.project_list')
next = restrict_next(next, domain=COOKIE_DOMAIN)
return redirect(next)
details_fields = ["name", "homepage", "description", "start_date",
"end_date", "comments"]
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment