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

wip Restructure astakos views

Replace custom transaction context with:

- decorator `commit_on_success_strict', which uses
  transaction.commit_manually and ensures that
  the transaction rolls back on any exception

- context ExceptionHandler, which logs and
  suppresses exceptions
parent b65492a3
......@@ -31,41 +31,24 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from synnefo.lib.db.xctx import TransactionHandler
from time import sleep
from django.contrib import messages
from django.utils.translation import ugettext as _
import astakos.im.messages as astakos_messages
import logging
logger = logging.getLogger(__name__)
class RetryException(Exception):
pass
class RetryTransactionHandler(TransactionHandler):
def __init__(self, retries=3, retry_wait=1.0, on_fail=None, **kwargs):
self.retries = retries
self.retry_wait = retry_wait
self.on_fail = on_fail
TransactionHandler.__init__(self, **kwargs)
class ExceptionHandler(object):
def __init__(self, request):
self.request = request
def __enter__(self):
pass
def __call__(self, func):
def wrap(*args, **kwargs):
while True:
try:
f = TransactionHandler.__call__(self, func)
return f(*args, **kwargs)
except RetryException as e:
self.retries -= 1
if self.retries <= 0:
logger.exception(e)
f = self.on_fail
if not callable(f):
raise
return f(*args, **kwargs)
sleep(self.retry_wait)
except BaseException as e:
logger.exception(e)
f = self.on_fail
if not callable(f):
raise
return f(*args, **kwargs)
return wrap
def __exit__(self, type, value, traceback):
if value is not None: # exception
logger.exception(value)
m = _(astakos_messages.GENERIC_ERROR)
messages.error(self.request, m)
return True # suppress exception
......@@ -36,7 +36,7 @@ from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from astakos.im.functions import (terminate, suspend, resume, check_expiration,
approve_application, deny_application)
from astakos.im.project_xctx import cmd_project_transaction_context
from synnefo.lib.db.transaction import commit_on_success_strict
class Command(BaseCommand):
......@@ -117,13 +117,13 @@ class Command(BaseCommand):
self.expire(execute=True)
def run_command(self, func, *args):
with cmd_project_transaction_context(sync=True) as ctx:
@commit_on_success_strict()
def inner():
try:
func(*args)
except BaseException as e:
if ctx:
ctx.mark_rollback()
raise CommandError(e)
inner()
def print_expired(self, projects, execute):
length = len(projects)
......@@ -152,7 +152,7 @@ class Command(BaseCommand):
self.stdout.write('%d projects have been terminated.\n' % (
length,))
@cmd_project_transaction_context(sync=True)
@commit_on_success_strict()
def expire(self, execute=False, ctx=None):
try:
projects = check_expiration(execute=execute)
......
# Copyright 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from synnefo.lib.db.xctx import TransactionContext
from astakos.im.retry_xctx import RetryTransactionHandler
from astakos.im.notifications import Notification
# USAGE
# =====
# @notification_transaction_context(notify=False)
# def a_view(args, ctx=None):
# ...
# if ctx:
# ctx.mark_rollback()
# ...
# return http response
#
# OR (more cleanly)
#
# def a_view(args):
# with notification_transaction_context(notify=False) as ctx:
# ...
# ctx.mark_rollback()
# ...
# return http response
def notification_transaction_context(**kwargs):
return RetryTransactionHandler(ctx=NotificationTransactionContext, **kwargs)
class NotificationTransactionContext(TransactionContext):
def __init__(self, notify=True, **kwargs):
self._notifications = []
self._messages = []
self._notify = notify
TransactionContext.__init__(self, **kwargs)
def register(self, o):
if isinstance(o, dict):
msg = o.get('msg', None)
if msg is not None:
if isinstance(msg, basestring):
self.queue_message(msg)
notif = o.get('notif', None)
if notif is not None:
if isinstance(notif, Notification):
self.queue_notification(notif)
if o.has_key('value'):
return o['value']
return o
def queue_message(self, m):
self._messages.append(m)
def queue_notification(self, n):
self._notifications.append(n)
def _send_notifications(self):
if self._notifications is None:
return
# send mail
def postprocess(self):
if self._notify:
self._send_notifications()
TransactionContext.postprocess(self)
......@@ -80,7 +80,6 @@ from astakos.im.models import (
AstakosUser, ApprovalTerms,
EmailChange, RESOURCE_SEPARATOR,
AstakosUserAuthProvider, PendingThirdPartyUser,
PendingMembershipError,
ProjectApplication, ProjectMembership, Project)
from astakos.im.util import (
get_context, prepare_response, get_query, restrict_next)
......@@ -115,8 +114,8 @@ from astakos.im.api import get_services_dict
from astakos.im import settings as astakos_settings
from astakos.im.api.callpoint import AstakosCallpoint
from astakos.im import auth_providers as auth
from astakos.im.project_xctx import project_transaction_context
from astakos.im.retry_xctx import RetryException
from synnefo.lib.db.transaction import commit_on_success_strict
from astakos.im.ctx import ExceptionHandler
logger = logging.getLogger(__name__)
......@@ -931,11 +930,12 @@ def how_it_works(request):
'im/how_it_works.html',
context_instance=get_context(request))
@project_transaction_context()
@commit_on_success_strict()
def _create_object(request, model=None, template_name=None,
template_loader=template_loader, extra_context=None, post_save_redirect=None,
login_required=False, context_processors=None, form_class=None,
msg=None, ctx=None):
msg=None):
"""
Based of django.views.generic.create_update.create_object which displays a
summary page before creating the object.
......@@ -968,12 +968,10 @@ def _create_object(request, model=None, template_name=None,
response = redirect(post_save_redirect, new_object)
else:
form = form_class()
except BaseException, e:
logger.exception(e)
messages.error(request, _(astakos_messages.GENERIC_ERROR))
if ctx:
ctx.mark_rollback()
finally:
except (IOError, PermissionDenied), e:
messages.error(request, e)
return None
else:
if response == None:
# Create the template, context, response
if not template_name:
......@@ -987,12 +985,12 @@ def _create_object(request, model=None, template_name=None,
response = HttpResponse(t.render(c))
return response
@project_transaction_context()
@commit_on_success_strict()
def _update_object(request, model=None, object_id=None, slug=None,
slug_field='slug', template_name=None, template_loader=template_loader,
extra_context=None, post_save_redirect=None, login_required=False,
context_processors=None, template_object_name='object',
form_class=None, msg=None, ctx=None):
form_class=None, msg=None):
"""
Based of django.views.generic.create_update.update_object which displays a
summary page before updating the object.
......@@ -1026,11 +1024,10 @@ def _update_object(request, model=None, object_id=None, slug=None,
response = redirect(post_save_redirect, obj)
else:
form = form_class(instance=obj)
except BaseException, e:
logger.exception(e)
messages.error(request, _(astakos_messages.GENERIC_ERROR))
ctx.mark_rollback()
finally:
except (IOError, PermissionDenied), e:
messages.error(request, e)
return None
else:
if response == None:
if not template_name:
template_name = "%s/%s_form.html" %\
......@@ -1094,14 +1091,25 @@ def project_add(request):
'show_form':True,
'details_fields':details_fields,
'membership_fields':membership_fields}
return _create_object(
request,
template_name='im/projects/projectapplication_form.html',
extra_context=extra_context,
post_save_redirect=reverse('project_list'),
form_class=ProjectApplicationForm,
msg=_("The %(verbose_name)s has been received and \
is under consideration."))
response = None
with ExceptionHandler(request):
response = _create_object(
request,
template_name='im/projects/projectapplication_form.html',
extra_context=extra_context,
post_save_redirect=reverse('project_list'),
form_class=ProjectApplicationForm,
msg=_("The %(verbose_name)s has been received and "
"is under consideration."),
)
if response is not None:
return response
next = reverse('astakos.im.views.project_list')
next = restrict_next(next, domain=COOKIE_DOMAIN)
return redirect(next)
@require_http_methods(["GET"])
......@@ -1124,25 +1132,13 @@ def project_list(request):
@require_http_methods(["POST"])
@valid_astakos_user_required
@project_transaction_context()
def project_app_cancel(request, application_id, ctx=None):
def project_app_cancel(request, application_id):
next = request.GET.get('next')
chain_id = None
try:
application_id = int(application_id)
chain_id = get_related_project_id(application_id)
cancel_application(application_id, request.user)
except (IOError, PermissionDenied), e:
messages.error(request, e)
except BaseException, e:
logger.exception(e)
messages.error(request, _(astakos_messages.GENERIC_ERROR))
if ctx:
ctx.mark_rollback()
else:
msg = _(astakos_messages.APPLICATION_CANCELLED)
messages.success(request, msg)
next = request.GET.get('next')
with ExceptionHandler(request):
chain_id = _project_app_cancel(request, application_id)
if not next:
if chain_id:
next = reverse('astakos.im.views.project_detail', args=(chain_id,))
......@@ -1152,6 +1148,20 @@ def project_app_cancel(request, application_id, ctx=None):
next = restrict_next(next, domain=COOKIE_DOMAIN)
return redirect(next)
@commit_on_success_strict()
def _project_app_cancel(request, application_id):
chain_id = None
try:
application_id = int(application_id)
chain_id = get_related_project_id(application_id)
cancel_application(application_id, request.user)
except (IOError, PermissionDenied), e:
messages.error(request, e)
else:
msg = _(astakos_messages.APPLICATION_CANCELLED)
messages.success(request, msg)
return chain_id
@require_http_methods(["GET", "POST"])
@valid_astakos_user_required
......@@ -1199,15 +1209,25 @@ def project_modify(request, application_id):
'details_fields':details_fields,
'update_form': True,
'membership_fields':membership_fields}
return _update_object(
request,
object_id=application_id,
template_name='im/projects/projectapplication_form.html',
extra_context=extra_context, post_save_redirect=reverse('project_list'),
form_class=ProjectApplicationForm,
msg = _("The %(verbose_name)s has been received and \
is under consideration."))
response = None
with ExceptionHandler(request):
response =_update_object(
request,
object_id=application_id,
template_name='im/projects/projectapplication_form.html',
extra_context=extra_context, post_save_redirect=reverse('project_list'),
form_class=ProjectApplicationForm,
msg = _("The %(verbose_name)s has been received and "
"is under consideration."),
)
if response is not None:
return response
next = reverse('astakos.im.views.project_list')
next = restrict_next(next, domain=COOKIE_DOMAIN)
return redirect(next)
@require_http_methods(["GET", "POST"])
@valid_astakos_user_required
......@@ -1219,8 +1239,8 @@ def project_app(request, application_id):
def project_detail(request, chain_id):
return common_detail(request, chain_id)
@project_transaction_context(sync=True)
def addmembers(request, chain_id, addmembers_form, ctx=None):
@commit_on_success_strict()
def addmembers(request, chain_id, addmembers_form):
if addmembers_form.is_valid():
try:
chain_id = int(chain_id)
......@@ -1231,10 +1251,6 @@ def addmembers(request, chain_id, addmembers_form, ctx=None):
addmembers_form.valid_users)
except (IOError, PermissionDenied), e:
messages.error(request, e)
except BaseException, e:
if ctx:
ctx.mark_rollback()
messages.error(request, e)
def common_detail(request, chain_or_app_id, project_view=True):
project = None
......@@ -1245,7 +1261,9 @@ def common_detail(request, chain_or_app_id, project_view=True):
request.POST,
chain_id=int(chain_id),
request_user=request.user)
addmembers(request, chain_id, addmembers_form)
with ExceptionHandler(request):
addmembers(request, chain_id, addmembers_form)
if addmembers_form.is_valid():
addmembers_form = AddProjectMembersForm() # clear form data
else:
......@@ -1357,13 +1375,22 @@ def project_search(request):
@require_http_methods(["POST"])
@valid_astakos_user_required
@project_transaction_context(sync=True)
def project_join(request, chain_id, ctx=None):
def project_join(request, chain_id):
next = request.GET.get('next')
if not next:
next = reverse('astakos.im.views.project_detail',
args=(chain_id,))
with ExceptionHandler(request):
_project_join(request, chain_id)
next = restrict_next(next, domain=COOKIE_DOMAIN)
return redirect(next)
@commit_on_success_strict()
def _project_join(request, chain_id):
try:
chain_id = int(chain_id)
auto_accepted = join_project(chain_id, request.user)
......@@ -1374,22 +1401,24 @@ def project_join(request, chain_id, ctx=None):
messages.success(request, m)
except (IOError, PermissionDenied), e:
messages.error(request, e)
except BaseException, e:
logger.exception(e)
messages.error(request, _(astakos_messages.GENERIC_ERROR))
if ctx:
ctx.mark_rollback()
next = restrict_next(next, domain=COOKIE_DOMAIN)
return redirect(next)
@require_http_methods(["POST"])
@valid_astakos_user_required
@project_transaction_context(sync=True)
def project_leave(request, chain_id, ctx=None):
def project_leave(request, chain_id):
next = request.GET.get('next')
if not next:
next = reverse('astakos.im.views.project_list')
with ExceptionHandler(request):
_project_leave(request, chain_id)
next = restrict_next(next, domain=COOKIE_DOMAIN)
return redirect(next)
@commit_on_success_strict()
def _project_leave(request, chain_id):
try:
chain_id = int(chain_id)
auto_accepted = leave_project(chain_id, request.user)
......@@ -1400,24 +1429,24 @@ def project_leave(request, chain_id, ctx=None):
messages.success(request, m)
except (IOError, PermissionDenied), e:
messages.error(request, e)
except PendingMembershipError as e:
raise RetryException()
except BaseException, e:
logger.exception(e)
messages.error(request, _(astakos_messages.GENERIC_ERROR))
if ctx:
ctx.mark_rollback()
next = restrict_next(next, domain=COOKIE_DOMAIN)
return redirect(next)
@require_http_methods(["POST"])
@valid_astakos_user_required
@project_transaction_context()
def project_cancel(request, chain_id, ctx=None):
def project_cancel(request, chain_id):
next = request.GET.get('next')
if not next:
next = reverse('astakos.im.views.project_list')
with ExceptionHandler(request):
_project_cancel(request, chain_id)
next = restrict_next(next, domain=COOKIE_DOMAIN)
return redirect(next)
@commit_on_success_strict()
def _project_cancel(request, chain_id):
try:
chain_id = int(chain_id)
cancel_membership(chain_id, request.user)
......@@ -1425,91 +1454,84 @@ def project_cancel(request, chain_id, ctx=None):
messages.success(request, m)
except (IOError, PermissionDenied), e:
messages.error(request, e)
except PendingMembershipError as e:
raise RetryException()
except BaseException, e:
logger.exception(e)
messages.error(request, _(astakos_messages.GENERIC_ERROR))
if ctx:
ctx.mark_rollback()
next = restrict_next(next, domain=COOKIE_DOMAIN)
return redirect(next)
@require_http_methods(["POST"])
@valid_astakos_user_required
@project_transaction_context(sync=True)
def project_accept_member(request, chain_id, user_id, ctx=None):
def project_accept_member(request, chain_id, user_id):
with ExceptionHandler(request):
_project_accept_member(request, chain_id, user_id)
return redirect(reverse('project_detail', args=(chain_id,)))
@commit_on_success_strict()
def _project_accept_member(request, chain_id, user_id):
try:
chain_id = int(chain_id)
user_id = int(user_id)
m = accept_membership(chain_id, user_id, request.user)
except (IOError, PermissionDenied), e:
messages.error(request, e)
except PendingMembershipError as e:
raise RetryException()
except BaseException, e:
logger.exception(e)
messages.error(request, _(astakos_messages.GENERIC_ERROR))
if ctx:
ctx.mark_rollback()
else:
email = escape(m.person.email)
msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
messages.success(request, msg)
return redirect(reverse('project_detail', args=(chain_id,)))
@require_http_methods(["POST"])
@valid_astakos_user_required
@project_transaction_context(sync=True)
def project_remove_member(request, chain_id, user_id, ctx=None):
def project_remove_member(request, chain_id, user_id):
with ExceptionHandler(request):
_project_remove_member(request, chain_id, user_id)
return redirect(reverse('project_detail', args=(chain_id,)))
@commit_on_success_strict()
def _project_remove_member(request, chain_id, user_id):
try:
chain_id = int(chain_id)
user_id = int(user_id)
m = remove_membership(chain_id, user_id, request.user)
except (IOError, PermissionDenied), e:
messages.error(request, e)
except PendingMembershipError as e:
raise RetryException()
except BaseException, e:
logger.exception(e)
messages.error(request, _(astakos_messages.GENERIC_ERROR))
if ctx:
ctx.mark_rollback()
else:
email = escape(m.person.email)
msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email
messages.success(request, msg)
return redirect(reverse('project_detail', args=(chain_id,)))
@require_http_methods(["POST"])
@valid_astakos_user_required
@project_transaction_context()