Commit ad8e73bb authored by Giorgos Korfiatis's avatar Giorgos Korfiatis Committed by Georgios D. Tsoukalas
Browse files

Add per-user limit on pending applications

Add UserSettings model for storing integer-valued settings.
If an entry is missing, a default synnefo setting is consulted.

The limit can be set/unset with snf-manage user-update.
parent cf6d65c3
...@@ -54,6 +54,7 @@ from smtplib import SMTPException ...@@ -54,6 +54,7 @@ from smtplib import SMTPException
from datetime import datetime from datetime import datetime
from functools import wraps from functools import wraps
import astakos.im.settings as astakos_settings
from astakos.im.settings import ( from astakos.im.settings import (
DEFAULT_CONTACT_EMAIL, SITENAME, BASEURL, LOGGING_LEVEL, DEFAULT_CONTACT_EMAIL, SITENAME, BASEURL, LOGGING_LEVEL,
VERIFICATION_EMAIL_SUBJECT, ACCOUNT_CREATION_SUBJECT, VERIFICATION_EMAIL_SUBJECT, ACCOUNT_CREATION_SUBJECT,
...@@ -67,6 +68,7 @@ from astakos.im.settings import ( ...@@ -67,6 +68,7 @@ from astakos.im.settings import (
from astakos.im.notifications import build_notification, NotificationError from astakos.im.notifications import build_notification, NotificationError
from astakos.im.models import ( from astakos.im.models import (
AstakosUser, Invitation, ProjectMembership, ProjectApplication, Project, AstakosUser, Invitation, ProjectMembership, ProjectApplication, Project,
UserSetting,
PendingMembershipError, get_resource_names, new_chain) PendingMembershipError, get_resource_names, new_chain)
from astakos.im.project_notif import ( from astakos.im.project_notif import (
membership_change_notify, membership_enroll_notify, membership_change_notify, membership_enroll_notify,
...@@ -694,6 +696,11 @@ def submit_application(kw, request_user=None): ...@@ -694,6 +696,11 @@ def submit_application(kw, request_user=None):
m = _(astakos_messages.NOT_ALLOWED) m = _(astakos_messages.NOT_ALLOWED)
raise PermissionDenied(m) raise PermissionDenied(m)
reached, limit = reached_pending_application_limit(request_user.id, precursor)
if reached:
m = _(astakos_messages.REACHED_PENDING_APPLICATION_LIMIT) % limit
raise PermissionDenied(m)
application = ProjectApplication(**kw) application = ProjectApplication(**kw)
if precursor is None: if precursor is None:
...@@ -808,3 +815,70 @@ def get_by_chain_or_404(chain_id): ...@@ -808,3 +815,70 @@ def get_by_chain_or_404(chain_id):
raise Http404 raise Http404
else: else:
return None, application return None, application
def get_user_setting(user_id, key):
try:
setting = UserSetting.objects.get(
user=user_id, setting=key)
return setting.value
except UserSetting.DoesNotExist:
return getattr(astakos_settings, key)
def set_user_setting(user_id, key, value):
try:
setting = UserSetting.objects.get_for_update(
user=user_id, setting=key)
except UserSetting.DoesNotExist:
setting = UserSetting(user_id=user_id, setting=key)
setting.value = value
setting.save()
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(
applicant__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)
if precursor is None:
return reached, limit
chain = precursor.chain
objs = ProjectApplication.objects
q = objs.filter(chain=chain, state=ProjectApplication.PENDING)
has_pending = q.exists()
if not has_pending:
return reached, limit
return False, limit
...@@ -71,6 +71,11 @@ class Command(BaseCommand): ...@@ -71,6 +71,11 @@ class Command(BaseCommand):
for resource, limits in quotas.iteritems(): for resource, limits in quotas.iteritems():
showable_quotas[resource] = limits.capacity showable_quotas[resource] = limits.capacity
settings_dict = {}
settings = user.settings()
for setting in settings:
settings_dict[setting.setting] = setting.value
kv = OrderedDict( kv = OrderedDict(
[ [
('id', user.id), ('id', user.id),
...@@ -97,9 +102,13 @@ class Command(BaseCommand): ...@@ -97,9 +102,13 @@ class Command(BaseCommand):
('email_verified', user.email_verified), ('email_verified', user.email_verified),
('username', user.username), ('username', user.username),
('activation_sent_date', user.activation_sent), ('activation_sent_date', user.activation_sent),
('resources', showable_quotas),
]) ])
if settings_dict:
kv['settings'] = settings_dict
kv['resources'] = showable_quotas
if get_latest_terms(): if get_latest_terms():
has_signed_terms = user.signed_terms has_signed_terms = user.signed_terms
kv['has_signed_terms'] = has_signed_terms kv['has_signed_terms'] = has_signed_terms
......
...@@ -34,12 +34,15 @@ ...@@ -34,12 +34,15 @@
from optparse import make_option from optparse import make_option
from datetime import datetime from datetime import datetime
from django.utils.translation import ugettext as _
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from astakos.im.models import AstakosUser from astakos.im.models import AstakosUser
from astakos.im.functions import activate, deactivate from astakos.im.functions import (activate, deactivate,
set_pending_application_limit,
unset_pending_application_limit)
from ._common import remove_user_permission, add_user_permission from ._common import remove_user_permission, add_user_permission
...@@ -102,6 +105,17 @@ class Command(BaseCommand): ...@@ -102,6 +105,17 @@ class Command(BaseCommand):
make_option('--delete-permission', make_option('--delete-permission',
dest='delete-permission', dest='delete-permission',
help="Delete user permission"), help="Delete user permission"),
make_option('--set-max-pending',
dest='pending',
metavar='INT',
help=("Set limit on user's maximum pending "
"project applications")),
make_option('--unset-max-pending',
dest='unset_pending',
action='store_true',
default=False,
help=("Restore default limit of user's maximum pending "
"project applications")),
) )
def handle(self, *args, **options): def handle(self, *args, **options):
...@@ -109,7 +123,8 @@ class Command(BaseCommand): ...@@ -109,7 +123,8 @@ class Command(BaseCommand):
raise CommandError("Please provide a user ID") raise CommandError("Please provide a user ID")
if args[0].isdigit(): if args[0].isdigit():
user = AstakosUser.objects.get(id=int(args[0])) user_id = int(args[0])
user = AstakosUser.objects.get(id=user_id)
else: else:
raise CommandError("Invalid ID") raise CommandError("Invalid ID")
...@@ -202,3 +217,17 @@ class Command(BaseCommand): ...@@ -202,3 +217,17 @@ class Command(BaseCommand):
if password: if password:
self.stdout.write('User\'s new password: %s\n' % 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)
...@@ -194,6 +194,20 @@ APPLICATION_CANNOT_DISMISS = "Cannot dismiss application %s in st ...@@ -194,6 +194,20 @@ APPLICATION_CANNOT_DISMISS = "Cannot dismiss application %s in st
APPLICATION_CANNOT_CANCEL = "Cannot cancel application %s in state '%s'" APPLICATION_CANNOT_CANCEL = "Cannot cancel application %s in state '%s'"
APPLICATION_CANCELLED = "Your project request has been cancelled." APPLICATION_CANCELLED = "Your project request has been cancelled."
REACHED_PENDING_APPLICATION_LIMIT = ("You have reached the maximum number "
"of pending project applications: %s.")
PENDING_APPLICATION_LIMIT_ADD = \
("You are not allowed to create a new project "
"because you have reached the maximum [%s] for "
"pending project applications. "
"Consider cancelling any unnecessary ones.")
PENDING_APPLICATION_LIMIT_MODIFY = \
("You are not allowed to modify this project "
"because you have reached the maximum [%s] for "
"pending project applications. "
"Consider cancelling any unnecessary ones.")
# Auth providers messages # Auth providers messages
AUTH_PROVIDER_NOT_ACTIVE = "'%(provider)s' is disabled." AUTH_PROVIDER_NOT_ACTIVE = "'%(provider)s' is disabled."
......
...@@ -837,6 +837,9 @@ class AstakosUser(User): ...@@ -837,6 +837,9 @@ class AstakosUser(User):
return False return False
return True return True
def settings(self):
return UserSetting.objects.filter(user=self)
class AstakosUserAuthProviderManager(models.Manager): class AstakosUserAuthProviderManager(models.Manager):
...@@ -1195,6 +1198,17 @@ class SessionCatalog(models.Model): ...@@ -1195,6 +1198,17 @@ class SessionCatalog(models.Model):
user = models.ForeignKey(AstakosUser, related_name='sessions', null=True) user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
class UserSetting(models.Model):
user = models.ForeignKey(AstakosUser)
setting = models.CharField(max_length=255)
value = models.IntegerField()
objects = ForUpdateManager()
class Meta:
unique_together = ("user", "setting")
### PROJECTS ### ### PROJECTS ###
################ ################
......
...@@ -340,6 +340,12 @@ TRANSLATE_UUIDS = getattr(settings, 'ASTAKOS_TRANSLATE_UUIDS', False) ...@@ -340,6 +340,12 @@ TRANSLATE_UUIDS = getattr(settings, 'ASTAKOS_TRANSLATE_UUIDS', False)
# Users that can approve or deny project applications from the web. # Users that can approve or deny project applications from the web.
PROJECT_ADMINS = getattr(settings, 'ASTAKOS_PROJECT_ADMINS', set()) PROJECT_ADMINS = getattr(settings, 'ASTAKOS_PROJECT_ADMINS', set())
# Maximum pending project applications per applicant.
# This is to reduce the volume of applications
# in case users abuse the mechanism.
PENDING_APPLICATION_LIMIT = getattr(settings,
'ASTAKOS_PENDING_APPLICATION_LIMIT', 1)
# OAuth2 Twitter credentials. # OAuth2 Twitter credentials.
TWITTER_TOKEN = getattr(settings, 'ASTAKOS_TWITTER_TOKEN', '') TWITTER_TOKEN = getattr(settings, 'ASTAKOS_TWITTER_TOKEN', '')
TWITTER_SECRET = getattr(settings, 'ASTAKOS_TWITTER_SECRET', '') TWITTER_SECRET = getattr(settings, 'ASTAKOS_TWITTER_SECRET', '')
......
...@@ -99,6 +99,7 @@ from astakos.im.functions import ( ...@@ -99,6 +99,7 @@ from astakos.im.functions import (
invite, invite,
send_activation as send_activation_func, send_activation as send_activation_func,
SendNotificationError, SendNotificationError,
reached_pending_application_limit,
accept_membership, reject_membership, remove_membership, cancel_membership, accept_membership, reject_membership, remove_membership, cancel_membership,
leave_project, join_project, enroll_member, can_join_request, can_leave_request, leave_project, join_project, enroll_member, can_join_request, can_leave_request,
get_related_project_id, get_by_chain_or_404, get_related_project_id, get_by_chain_or_404,
...@@ -1045,6 +1046,16 @@ def _update_object(request, model=None, object_id=None, slug=None, ...@@ -1045,6 +1046,16 @@ def _update_object(request, model=None, object_id=None, slug=None,
@signed_terms_required @signed_terms_required
@login_required @login_required
def project_add(request): def project_add(request):
user = request.user
reached, limit = reached_pending_application_limit(user.id)
if 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)
resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {}) resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
resource_catalog = () resource_catalog = ()
result = callpoint.list_resources() result = callpoint.list_resources()
...@@ -1157,6 +1168,14 @@ def project_modify(request, application_id): ...@@ -1157,6 +1168,14 @@ def project_modify(request, application_id):
m = _(astakos_messages.NOT_ALLOWED) m = _(astakos_messages.NOT_ALLOWED)
raise PermissionDenied(m) raise PermissionDenied(m)
reached, limit = reached_pending_application_limit(user.id, app)
if 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)
resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {}) resource_groups = RESOURCES_PRESENTATION_DATA.get('groups', {})
resource_catalog = () resource_catalog = ()
result = callpoint.list_resources() result = callpoint.list_resources()
......
...@@ -283,6 +283,11 @@ ...@@ -283,6 +283,11 @@
# UUIDs of users that can approve or deny project applications from the web. # UUIDs of users that can approve or deny project applications from the web.
# ASTAKOS_PROJECT_ADMINS = set() # e.g. set(['01234567-89ab-cdef-0123-456789abcdef']) # ASTAKOS_PROJECT_ADMINS = set() # e.g. set(['01234567-89ab-cdef-0123-456789abcdef'])
# Maximum pending project applications per applicant.
# This is to reduce the volume of applications
# in case users abuse the mechanism.
#ASTAKOS_PENDING_APPLICATION_LIMIT = 1
# OAuth2 Twitter credentials. # OAuth2 Twitter credentials.
#ASTAKOS_TWITTER_KEY = '' #ASTAKOS_TWITTER_KEY = ''
#ASTAKOS_TWITTER_SECRET = '' #ASTAKOS_TWITTER_SECRET = ''
......
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