Commit 10049d18 authored by Georgios D. Tsoukalas's avatar Georgios D. Tsoukalas
Browse files

approve, deny, dismiss views

At the project application detail, project administrators
(ASTAKOS_PROJECT_ADMINS setting) may approve or deny it.
A denied application is still viewable by the applicant,
until he dismisses it from the same page.

The project administrators' project list includes all projects.
parent 7ee1fd1a
......@@ -68,7 +68,7 @@ from astakos.im.settings import (
DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
AUTH_TOKEN_DURATION, EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL,
SITENAME, SERVICES, MODERATION_ENABLED, RESOURCES_PRESENTATION_DATA,
PROJECT_MEMBER_JOIN_POLICIES, PROJECT_MEMBER_LEAVE_POLICIES)
PROJECT_MEMBER_JOIN_POLICIES, PROJECT_MEMBER_LEAVE_POLICIES, PROJECT_ADMINS)
from astakos.im import settings as astakos_settings
from astakos.im.endpoints.qh import (
register_users, send_quotas, qh_check_users, qh_get_quota_limits,
......@@ -428,6 +428,9 @@ class AstakosUser(User):
content_type=get_content_type())
self.user_permissions.remove(p)
def is_project_admin(self, application_id=None):
return self.uuid in PROJECT_ADMINS
@property
def invitation(self):
try:
......@@ -815,6 +818,8 @@ class AstakosUser(User):
return m.user_friendly_state_display()
def non_owner_can_view(self, maybe_project):
if self.is_project_admin():
return True
if maybe_project is None:
return False
project = maybe_project
......@@ -1422,8 +1427,11 @@ class ProjectApplicationManager(ForUpdateManager):
"""
Return projects accessed by specified user.
"""
participates_filters = Q(owner=user) | Q(applicant=user) | \
Q(project__projectmembership__person=user)
if user.is_project_admin():
participates_filters = Q()
else:
participates_filters = Q(owner=user) | Q(applicant=user) | \
Q(project__projectmembership__person=user)
return self.user_visible_by_chain(participates_filters).order_by('issue_date').distinct()
......@@ -1612,6 +1620,12 @@ class ProjectApplication(models.Model):
except Project.DoesNotExist:
return None
def can_be_approved(self):
return self.state == self.PENDING
def can_be_dismissed(self):
return self.state == self.DENIED
def can_cancel(self):
return self.state == self.PENDING
......
......@@ -331,3 +331,6 @@ PROJECT_MEMBER_LEAVE_POLICIES = getattr(settings,
ACTIVATION_REDIRECT_URL = getattr(settings,
'ASTAKOS_ACTIVATION_REDIRECT_URL',
"/im/landing")
# Users that can approve or deny project applications from the web.
PROJECT_ADMINS = getattr(settings, 'ASTAKOS_PROJECT_ADMINS', set())
......@@ -286,7 +286,7 @@ class UserProjectApplicationsTable(UserTable):
project = record.project
return self.user.membership_display(project)
except Project.DoesNotExist:
return _(Unknown)
return _("Unknown")
def render_members_count(self, record, *args, **kwargs):
append = ""
......
......@@ -7,7 +7,7 @@
<div class="projects">
<h2>
<em>
{% if owner_mode %}
{% if owner_mode or admin_mode %}
{% if project_view %}
PROJECT {{ object.project_state_display|upper }}
{% if object.has_pending_modifications %} -
......@@ -43,25 +43,45 @@
</span>
<!-- make room for buttons -->
{% if owner_mode or can_join_request or can_leave_request %}
{% if owner_mode or admin_mode or can_join_request or can_leave_request %}
<br />
{% endif %}
{% if owner_mode %}
{% if owner_mode or admin_mode %}
<a style="font-size:0.7em"
href="{% url astakos.im.views.project_modify object.pk %}">MODIFY</a>
{% with object.last_pending_incl_me as last_pending %}
{% if last_pending %}
-
<a style="font-size:0.7em"
href="{% url astakos.im.views.project_app_cancel last_pending.pk %}">
CANCEL {% if object.project_exists %} MODIFICATION {% else %}
PROJECT {% endif %} REQUEST
</a>
{% if owner_mode %}
{% with object.last_pending_incl_me as last_pending %}
{% if last_pending %}
-
<a style="font-size:0.7em"
href="{% url astakos.im.views.project_app_cancel last_pending.pk %}">
CANCEL {% if object.project_exists %} MODIFICATION {% else %}
PROJECT {% endif %} REQUEST
</a>
{% endif %}
{% endwith %}
{% endif %}
{% endwith %}
{% if admin_mode %}
{% if object.can_be_approved %}
- <a style="font-size:0.7em"
href="{% url astakos.im.views.project_app_approve object.pk %}">
APPROVE</a>
- <a style="font-size:0.7em"
href="{% url astakos.im.views.project_app_deny object.pk %}">
DENY</a>
{% endif %}
{% endif %}
{% if owner_mode %}
{% if object.can_be_dismissed %}
- <a style="font-size:0.7em"
href="{% url astakos.im.views.project_app_dismiss object.pk %}">
DISMISS</a>
{% endif %}
{% endif %}
<!-- only one is possible, perhaps add cancel button too -->
{% if can_join_request or can_leave_request %}
<br />
......
......@@ -71,6 +71,9 @@ urlpatterns = patterns(
url(r'^projects/(?P<chain_id>\d+)/(?P<user_id>\d+)/remove/?$', 'project_remove_member', {}, name='project_remove_member'),
url(r'^projects/app/(?P<application_id>\d+)/?$', 'project_app', {}, name='project_app'),
url(r'^projects/app/(?P<application_id>\d+)/modify$', 'project_modify', {}, name='project_modify'),
url(r'^projects/app/(?P<application_id>\d+)/approve$', 'project_app_approve', {}, name='project_app_approve'),
url(r'^projects/app/(?P<application_id>\d+)/deny$', 'project_app_deny', {}, name='project_app_deny'),
url(r'^projects/app/(?P<application_id>\d+)/dismiss$', 'project_app_dismiss', {}, name='project_app_dismiss'),
url(r'^projects/app/(?P<application_id>\d+)/cancel$', 'project_app_cancel', {}, name='project_app_cancel'),
url(r'^projects/how_it_works/?$', 'how_it_works', {}, name='how_it_works'),
......
......@@ -100,8 +100,9 @@ from astakos.im.functions import (
SendNotificationError,
accept_membership, reject_membership, remove_membership, cancel_membership,
leave_project, join_project, enroll_member, can_join_request, can_leave_request,
cancel_application, get_related_project_id,
get_by_chain_or_404)
get_related_project_id, get_by_chain_or_404,
approve_application, deny_application,
cancel_application, dismiss_application)
from astakos.im.settings import (
COOKIE_DOMAIN, LOGOUT_NEXT,
LOGGING_LEVEL, PAGINATE_BY,
......@@ -1246,6 +1247,7 @@ def common_detail(request, chain_or_app_id, project_view=True):
modifications_table = None
user = request.user
is_project_admin = user.is_project_admin(application_id=application.id)
is_owner = user.owns_application(application)
if not is_owner and not project_view:
m = _(astakos_messages.NOT_ALLOWED)
......@@ -1277,6 +1279,7 @@ def common_detail(request, chain_or_app_id, project_view=True):
'addmembers_form':addmembers_form,
'members_table': members_table,
'owner_mode': is_owner,
'admin_mode': is_project_admin,
'modifications_table': modifications_table,
'mem_display': mem_display,
'can_join_request': can_join_req,
......@@ -1473,6 +1476,61 @@ def project_reject_member(request, chain_id, user_id, ctx=None):
messages.success(request, msg)
return redirect(reverse('project_detail', args=(chain_id,)))
@require_http_methods(["POST", "GET"])
@signed_terms_required
@login_required
@project_transaction_context(sync=True)
def project_app_approve(request, application_id, ctx=None):
if not request.user.is_project_admin():
m = _(astakos_messages.NOT_ALLOWED)
raise PermissionDenied(m)
try:
app = ProjectApplication.objects.get(id=application_id)
except ProjectApplication.DoesNotExist:
raise Http404
approve_application(application_id)
chain_id = get_related_project_id(application_id)
return redirect(reverse('project_detail', args=(chain_id,)))
@require_http_methods(["POST", "GET"])
@signed_terms_required
@login_required
@project_transaction_context()
def project_app_deny(request, application_id, ctx=None):
if not request.user.is_project_admin():
m = _(astakos_messages.NOT_ALLOWED)
raise PermissionDenied(m)
try:
app = ProjectApplication.objects.get(id=application_id)
except ProjectApplication.DoesNotExist:
raise Http404
deny_application(application_id)
return redirect(reverse('project_list'))
@require_http_methods(["POST", "GET"])
@signed_terms_required
@login_required
@project_transaction_context()
def project_app_dismiss(request, application_id, ctx=None):
try:
app = ProjectApplication.objects.get(id=application_id)
except ProjectApplication.DoesNotExist:
raise Http404
if not request.user.owns_application(app):
m = _(astakos_messages.NOT_ALLOWED)
raise PermissionDenied(m)
# XXX: dismiss application also does authorization
dismiss_application(application_id, request_user=request.user)
return redirect(reverse('project_list'))
def landing(request):
return render_response(
'im/landing.html',
......
......@@ -139,4 +139,7 @@
# NEWPASSWD_INVALIDATE_TOKEN = getattr(settings, 'ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN', True)
# Permit local account migration
# ENABLE_LOCAL_ACCOUNT_MIGRATION = getattr(settings, 'ASTAKOS_ENABLE_LOCAL_ACCOUNT_MIGRATION', True)
\ No newline at end of file
# ENABLE_LOCAL_ACCOUNT_MIGRATION = getattr(settings, 'ASTAKOS_ENABLE_LOCAL_ACCOUNT_MIGRATION', True)
# 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'])
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