Revision 39b2cb50

b/snf-astakos-app/astakos/im/ctx.py
1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
from django.contrib import messages
35
from django.utils.translation import ugettext as _
36
import astakos.im.messages as astakos_messages
37
import logging
38

  
39
logger = logging.getLogger(__name__)
40

  
41

  
42
class ExceptionHandler(object):
43
    def __init__(self, request):
44
        self.request = request
45

  
46
    def __enter__(self):
47
        pass
48

  
49
    def __exit__(self, type, value, traceback):
50
        if value is not None:  # exception
51
            logger.exception(value)
52
            m = _(astakos_messages.GENERIC_ERROR)
53
            messages.error(self.request, m)
54
            return True  # suppress exception
b/snf-astakos-app/astakos/im/management/commands/project-control.py
36 36
from django.core.management.base import BaseCommand, CommandError
37 37
from astakos.im.functions import (terminate, suspend, resume, check_expiration,
38 38
                                  approve_application, deny_application)
39
from astakos.im.project_xctx import cmd_project_transaction_context
39
from synnefo.lib.db.transaction import commit_on_success_strict
40 40

  
41 41

  
42 42
class Command(BaseCommand):
......
117 117
            self.expire(execute=True)
118 118

  
119 119
    def run_command(self, func, *args):
120
        with cmd_project_transaction_context(sync=True) as ctx:
120
        @commit_on_success_strict()
121
        def inner():
121 122
            try:
122 123
                func(*args)
123 124
            except BaseException as e:
124
                if ctx:
125
                    ctx.mark_rollback()
126 125
                raise CommandError(e)
126
        inner()
127 127

  
128 128
    def print_expired(self, projects, execute):
129 129
        length = len(projects)
......
152 152
                self.stdout.write('%d projects have been terminated.\n' % (
153 153
                    length,))
154 154

  
155
    @cmd_project_transaction_context(sync=True)
155
    @commit_on_success_strict()
156 156
    def expire(self, execute=False, ctx=None):
157 157
        try:
158 158
            projects = check_expiration(execute=execute)
/dev/null
1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
from synnefo.lib.db.xctx import TransactionContext
35
from astakos.im.retry_xctx import RetryTransactionHandler
36
from astakos.im.notifications import Notification
37

  
38
# USAGE
39
# =====
40
# @notification_transaction_context(notify=False)
41
# def a_view(args, ctx=None):
42
#     ...
43
#     if ctx:
44
#         ctx.mark_rollback()
45
#     ...
46
#     return http response
47
#
48
# OR (more cleanly)
49
#
50
# def a_view(args):
51
#     with notification_transaction_context(notify=False) as ctx:
52
#         ...
53
#         ctx.mark_rollback()
54
#         ...
55
#         return http response
56

  
57
def notification_transaction_context(**kwargs):
58
    return RetryTransactionHandler(ctx=NotificationTransactionContext, **kwargs)
59

  
60

  
61
class NotificationTransactionContext(TransactionContext):
62
    def __init__(self, notify=True, **kwargs):
63
        self._notifications = []
64
        self._messages      = []
65
        self._notify        = notify
66
        TransactionContext.__init__(self, **kwargs)
67

  
68
    def register(self, o):
69
        if isinstance(o, dict):
70
            msg = o.get('msg', None)
71
            if msg is not None:
72
                if isinstance(msg, basestring):
73
                    self.queue_message(msg)
74

  
75
            notif = o.get('notif', None)
76
            if notif is not None:
77
                if isinstance(notif, Notification):
78
                    self.queue_notification(notif)
79

  
80
            if o.has_key('value'):
81
                return o['value']
82
        return o
83

  
84
    def queue_message(self, m):
85
        self._messages.append(m)
86

  
87
    def queue_notification(self, n):
88
        self._notifications.append(n)
89

  
90
    def _send_notifications(self):
91
        if self._notifications is None:
92
            return
93
        # send mail
94

  
95
    def postprocess(self):
96
        if self._notify:
97
            self._send_notifications()
98
        TransactionContext.postprocess(self)
/dev/null
1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
from astakos.im.retry_xctx import RetryTransactionHandler
35
from astakos.im.notification_xctx import NotificationTransactionContext
36
from astakos.im.models import sync_projects
37
from astakos.im.project_error import project_error_view
38

  
39
# USAGE
40
# =====
41
# @project_transaction_context(sync=True)
42
# def a_view(args, ctx=None):
43
#     ...
44
#     if ctx:
45
#         ctx.mark_rollback()
46
#     ...
47
#     return http response
48
#
49
# OR (more cleanly)
50
#
51
# def a_view(args):
52
#     with project_transaction_context(sync=True) as ctx:
53
#         ...
54
#         ctx.mark_rollback()
55
#         ...
56
#         return http response
57

  
58
def project_transaction_context(**kwargs):
59
    return RetryTransactionHandler(ctx=ProjectTransactionContext,
60
                                   on_fail=project_error_view,
61
                                   **kwargs)
62

  
63
def cmd_project_transaction_context(**kwargs):
64
    return RetryTransactionHandler(ctx=ProjectTransactionContext,
65
                                   **kwargs)
66

  
67
class ProjectTransactionContext(NotificationTransactionContext):
68
    def __init__(self, sync=False, **kwargs):
69
        self._sync = sync
70
        NotificationTransactionContext.__init__(self, **kwargs)
71

  
72
    def postprocess(self):
73
        if self._sync:
74
            sync_projects()
75
        NotificationTransactionContext.postprocess(self)
/dev/null
1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
from synnefo.lib.db.xctx import TransactionHandler
35
from time import sleep
36

  
37
import logging
38
logger = logging.getLogger(__name__)
39

  
40
class RetryException(Exception):
41
    pass
42

  
43
class RetryTransactionHandler(TransactionHandler):
44
    def __init__(self, retries=3, retry_wait=1.0, on_fail=None, **kwargs):
45
        self.retries    = retries
46
        self.retry_wait = retry_wait
47
        self.on_fail    = on_fail
48
        TransactionHandler.__init__(self, **kwargs)
49

  
50
    def __call__(self, func):
51
        def wrap(*args, **kwargs):
52
            while True:
53
                try:
54
                    f = TransactionHandler.__call__(self, func)
55
                    return f(*args, **kwargs)
56
                except RetryException as e:
57
                    self.retries -= 1
58
                    if self.retries <= 0:
59
                        logger.exception(e)
60
                        f = self.on_fail
61
                        if not callable(f):
62
                            raise
63
                        return f(*args, **kwargs)
64
                    sleep(self.retry_wait)
65
                except BaseException as e:
66
                    logger.exception(e)
67
                    f = self.on_fail
68
                    if not callable(f):
69
                        raise
70
                    return f(*args, **kwargs)
71
        return wrap
b/snf-astakos-app/astakos/im/views.py
80 80
    AstakosUser, ApprovalTerms,
81 81
    EmailChange, RESOURCE_SEPARATOR,
82 82
    AstakosUserAuthProvider, PendingThirdPartyUser,
83
    PendingMembershipError,
84 83
    ProjectApplication, ProjectMembership, Project)
85 84
from astakos.im.util import (
86 85
    get_context, prepare_response, get_query, restrict_next)
......
115 114
from astakos.im import settings as astakos_settings
116 115
from astakos.im.api.callpoint import AstakosCallpoint
117 116
from astakos.im import auth_providers as auth
118
from astakos.im.project_xctx import project_transaction_context
119
from astakos.im.retry_xctx import RetryException
117
from synnefo.lib.db.transaction import commit_on_success_strict
118
from astakos.im.ctx import ExceptionHandler
120 119

  
121 120
logger = logging.getLogger(__name__)
122 121

  
......
931 930
        'im/how_it_works.html',
932 931
        context_instance=get_context(request))
933 932

  
934
@project_transaction_context()
933

  
934
@commit_on_success_strict()
935 935
def _create_object(request, model=None, template_name=None,
936 936
        template_loader=template_loader, extra_context=None, post_save_redirect=None,
937 937
        login_required=False, context_processors=None, form_class=None,
938
        msg=None, ctx=None):
938
        msg=None):
939 939
    """
940 940
    Based of django.views.generic.create_update.create_object which displays a
941 941
    summary page before creating the object.
......
968 968
                    response = redirect(post_save_redirect, new_object)
969 969
        else:
970 970
            form = form_class()
971
    except BaseException, e:
972
        logger.exception(e)
973
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
974
        if ctx:
975
            ctx.mark_rollback()
976
    finally:
971
    except (IOError, PermissionDenied), e:
972
        messages.error(request, e)
973
        return None
974
    else:
977 975
        if response == None:
978 976
            # Create the template, context, response
979 977
            if not template_name:
......
987 985
            response = HttpResponse(t.render(c))
988 986
        return response
989 987

  
990
@project_transaction_context()
988
@commit_on_success_strict()
991 989
def _update_object(request, model=None, object_id=None, slug=None,
992 990
        slug_field='slug', template_name=None, template_loader=template_loader,
993 991
        extra_context=None, post_save_redirect=None, login_required=False,
994 992
        context_processors=None, template_object_name='object',
995
        form_class=None, msg=None, ctx=None):
993
        form_class=None, msg=None):
996 994
    """
997 995
    Based of django.views.generic.create_update.update_object which displays a
998 996
    summary page before updating the object.
......
1026 1024
                    response = redirect(post_save_redirect, obj)
1027 1025
        else:
1028 1026
            form = form_class(instance=obj)
1029
    except BaseException, e:
1030
        logger.exception(e)
1031
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1032
        ctx.mark_rollback()
1033
    finally:
1027
    except (IOError, PermissionDenied), e:
1028
        messages.error(request, e)
1029
        return None
1030
    else:
1034 1031
        if response == None:
1035 1032
            if not template_name:
1036 1033
                template_name = "%s/%s_form.html" %\
......
1094 1091
        'show_form':True,
1095 1092
        'details_fields':details_fields,
1096 1093
        'membership_fields':membership_fields}
1097
    return _create_object(
1098
        request,
1099
        template_name='im/projects/projectapplication_form.html',
1100
        extra_context=extra_context,
1101
        post_save_redirect=reverse('project_list'),
1102
        form_class=ProjectApplicationForm,
1103
        msg=_("The %(verbose_name)s has been received and \
1104
                 is under consideration."))
1094

  
1095
    response = None
1096
    with ExceptionHandler(request):
1097
        response = _create_object(
1098
            request,
1099
            template_name='im/projects/projectapplication_form.html',
1100
            extra_context=extra_context,
1101
            post_save_redirect=reverse('project_list'),
1102
            form_class=ProjectApplicationForm,
1103
            msg=_("The %(verbose_name)s has been received and "
1104
                  "is under consideration."),
1105
            )
1106

  
1107
    if response is not None:
1108
        return response
1109

  
1110
    next = reverse('astakos.im.views.project_list')
1111
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1112
    return redirect(next)
1105 1113

  
1106 1114

  
1107 1115
@require_http_methods(["GET"])
......
1124 1132

  
1125 1133
@require_http_methods(["POST"])
1126 1134
@valid_astakos_user_required
1127
@project_transaction_context()
1128
def project_app_cancel(request, application_id, ctx=None):
1135
def project_app_cancel(request, application_id):
1136
    next = request.GET.get('next')
1129 1137
    chain_id = None
1130
    try:
1131
        application_id = int(application_id)
1132
        chain_id = get_related_project_id(application_id)
1133
        cancel_application(application_id, request.user)
1134
    except (IOError, PermissionDenied), e:
1135
        messages.error(request, e)
1136
    except BaseException, e:
1137
        logger.exception(e)
1138
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1139
        if ctx:
1140
            ctx.mark_rollback()
1141
    else:
1142
        msg = _(astakos_messages.APPLICATION_CANCELLED)
1143
        messages.success(request, msg)
1144 1138

  
1145
    next = request.GET.get('next')
1139
    with ExceptionHandler(request):
1140
        chain_id = _project_app_cancel(request, application_id)
1141

  
1146 1142
    if not next:
1147 1143
        if chain_id:
1148 1144
            next = reverse('astakos.im.views.project_detail', args=(chain_id,))
......
1152 1148
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1153 1149
    return redirect(next)
1154 1150

  
1151
@commit_on_success_strict()
1152
def _project_app_cancel(request, application_id):
1153
    chain_id = None
1154
    try:
1155
        application_id = int(application_id)
1156
        chain_id = get_related_project_id(application_id)
1157
        cancel_application(application_id, request.user)
1158
    except (IOError, PermissionDenied), e:
1159
        messages.error(request, e)
1160
    else:
1161
        msg = _(astakos_messages.APPLICATION_CANCELLED)
1162
        messages.success(request, msg)
1163
        return chain_id
1164

  
1155 1165

  
1156 1166
@require_http_methods(["GET", "POST"])
1157 1167
@valid_astakos_user_required
......
1199 1209
        'details_fields':details_fields,
1200 1210
        'update_form': True,
1201 1211
        'membership_fields':membership_fields}
1202
    return _update_object(
1203
        request,
1204
        object_id=application_id,
1205
        template_name='im/projects/projectapplication_form.html',
1206
        extra_context=extra_context, post_save_redirect=reverse('project_list'),
1207
        form_class=ProjectApplicationForm,
1208
        msg = _("The %(verbose_name)s has been received and \
1209
                    is under consideration."))
1210 1212

  
1213
    response = None
1214
    with ExceptionHandler(request):
1215
        response =_update_object(
1216
            request,
1217
            object_id=application_id,
1218
            template_name='im/projects/projectapplication_form.html',
1219
            extra_context=extra_context, post_save_redirect=reverse('project_list'),
1220
            form_class=ProjectApplicationForm,
1221
            msg = _("The %(verbose_name)s has been received and "
1222
                    "is under consideration."),
1223
            )
1224

  
1225
    if response is not None:
1226
        return response
1227

  
1228
    next = reverse('astakos.im.views.project_list')
1229
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1230
    return redirect(next)
1211 1231

  
1212 1232
@require_http_methods(["GET", "POST"])
1213 1233
@valid_astakos_user_required
......
1219 1239
def project_detail(request, chain_id):
1220 1240
    return common_detail(request, chain_id)
1221 1241

  
1222
@project_transaction_context(sync=True)
1223
def addmembers(request, chain_id, addmembers_form, ctx=None):
1242
@commit_on_success_strict()
1243
def addmembers(request, chain_id, addmembers_form):
1224 1244
    if addmembers_form.is_valid():
1225 1245
        try:
1226 1246
            chain_id = int(chain_id)
......
1231 1251
                addmembers_form.valid_users)
1232 1252
        except (IOError, PermissionDenied), e:
1233 1253
            messages.error(request, e)
1234
        except BaseException, e:
1235
            if ctx:
1236
                ctx.mark_rollback()
1237
            messages.error(request, e)
1238 1254

  
1239 1255
def common_detail(request, chain_or_app_id, project_view=True):
1240 1256
    project = None
......
1245 1261
                request.POST,
1246 1262
                chain_id=int(chain_id),
1247 1263
                request_user=request.user)
1248
            addmembers(request, chain_id, addmembers_form)
1264
            with ExceptionHandler(request):
1265
                addmembers(request, chain_id, addmembers_form)
1266

  
1249 1267
            if addmembers_form.is_valid():
1250 1268
                addmembers_form = AddProjectMembersForm()  # clear form data
1251 1269
        else:
......
1357 1375

  
1358 1376
@require_http_methods(["POST"])
1359 1377
@valid_astakos_user_required
1360
@project_transaction_context(sync=True)
1361
def project_join(request, chain_id, ctx=None):
1378
def project_join(request, chain_id):
1362 1379
    next = request.GET.get('next')
1363 1380
    if not next:
1364 1381
        next = reverse('astakos.im.views.project_detail',
1365 1382
                       args=(chain_id,))
1366 1383

  
1384
    with ExceptionHandler(request):
1385
        _project_join(request, chain_id)
1386

  
1387

  
1388
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1389
    return redirect(next)
1390

  
1391

  
1392
@commit_on_success_strict()
1393
def _project_join(request, chain_id):
1367 1394
    try:
1368 1395
        chain_id = int(chain_id)
1369 1396
        auto_accepted = join_project(chain_id, request.user)
......
1374 1401
        messages.success(request, m)
1375 1402
    except (IOError, PermissionDenied), e:
1376 1403
        messages.error(request, e)
1377
    except BaseException, e:
1378
        logger.exception(e)
1379
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1380
        if ctx:
1381
            ctx.mark_rollback()
1382
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1383
    return redirect(next)
1404

  
1384 1405

  
1385 1406
@require_http_methods(["POST"])
1386 1407
@valid_astakos_user_required
1387
@project_transaction_context(sync=True)
1388
def project_leave(request, chain_id, ctx=None):
1408
def project_leave(request, chain_id):
1389 1409
    next = request.GET.get('next')
1390 1410
    if not next:
1391 1411
        next = reverse('astakos.im.views.project_list')
1392 1412

  
1413
    with ExceptionHandler(request):
1414
        _project_leave(request, chain_id)
1415

  
1416
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1417
    return redirect(next)
1418

  
1419

  
1420
@commit_on_success_strict()
1421
def _project_leave(request, chain_id):
1393 1422
    try:
1394 1423
        chain_id = int(chain_id)
1395 1424
        auto_accepted = leave_project(chain_id, request.user)
......
1400 1429
        messages.success(request, m)
1401 1430
    except (IOError, PermissionDenied), e:
1402 1431
        messages.error(request, e)
1403
    except PendingMembershipError as e:
1404
        raise RetryException()
1405
    except BaseException, e:
1406
        logger.exception(e)
1407
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1408
        if ctx:
1409
            ctx.mark_rollback()
1410
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1411
    return redirect(next)
1432

  
1412 1433

  
1413 1434
@require_http_methods(["POST"])
1414 1435
@valid_astakos_user_required
1415
@project_transaction_context()
1416
def project_cancel(request, chain_id, ctx=None):
1436
def project_cancel(request, chain_id):
1417 1437
    next = request.GET.get('next')
1418 1438
    if not next:
1419 1439
        next = reverse('astakos.im.views.project_list')
1420 1440

  
1441
    with ExceptionHandler(request):
1442
        _project_cancel(request, chain_id)
1443

  
1444
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1445
    return redirect(next)
1446

  
1447

  
1448
@commit_on_success_strict()
1449
def _project_cancel(request, chain_id):
1421 1450
    try:
1422 1451
        chain_id = int(chain_id)
1423 1452
        cancel_membership(chain_id, request.user)
......
1425 1454
        messages.success(request, m)
1426 1455
    except (IOError, PermissionDenied), e:
1427 1456
        messages.error(request, e)
1428
    except PendingMembershipError as e:
1429
        raise RetryException()
1430
    except BaseException, e:
1431
        logger.exception(e)
1432
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1433
        if ctx:
1434
            ctx.mark_rollback()
1435 1457

  
1436
    next = restrict_next(next, domain=COOKIE_DOMAIN)
1437
    return redirect(next)
1438 1458

  
1439 1459
@require_http_methods(["POST"])
1440 1460
@valid_astakos_user_required
1441
@project_transaction_context(sync=True)
1442
def project_accept_member(request, chain_id, user_id, ctx=None):
1461
def project_accept_member(request, chain_id, user_id):
1462

  
1463
    with ExceptionHandler(request):
1464
        _project_accept_member(request, chain_id, user_id)
1465

  
1466
    return redirect(reverse('project_detail', args=(chain_id,)))
1467

  
1468

  
1469
@commit_on_success_strict()
1470
def _project_accept_member(request, chain_id, user_id):
1443 1471
    try:
1444 1472
        chain_id = int(chain_id)
1445 1473
        user_id = int(user_id)
1446 1474
        m = accept_membership(chain_id, user_id, request.user)
1447 1475
    except (IOError, PermissionDenied), e:
1448 1476
        messages.error(request, e)
1449
    except PendingMembershipError as e:
1450
        raise RetryException()
1451
    except BaseException, e:
1452
        logger.exception(e)
1453
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1454
        if ctx:
1455
            ctx.mark_rollback()
1456 1477
    else:
1457 1478
        email = escape(m.person.email)
1458 1479
        msg = _(astakos_messages.USER_MEMBERSHIP_ACCEPTED) % email
1459 1480
        messages.success(request, msg)
1460
    return redirect(reverse('project_detail', args=(chain_id,)))
1481

  
1461 1482

  
1462 1483
@require_http_methods(["POST"])
1463 1484
@valid_astakos_user_required
1464
@project_transaction_context(sync=True)
1465
def project_remove_member(request, chain_id, user_id, ctx=None):
1485
def project_remove_member(request, chain_id, user_id):
1486

  
1487
    with ExceptionHandler(request):
1488
        _project_remove_member(request, chain_id, user_id)
1489

  
1490
    return redirect(reverse('project_detail', args=(chain_id,)))
1491

  
1492

  
1493
@commit_on_success_strict()
1494
def _project_remove_member(request, chain_id, user_id):
1466 1495
    try:
1467 1496
        chain_id = int(chain_id)
1468 1497
        user_id = int(user_id)
1469 1498
        m = remove_membership(chain_id, user_id, request.user)
1470 1499
    except (IOError, PermissionDenied), e:
1471 1500
        messages.error(request, e)
1472
    except PendingMembershipError as e:
1473
        raise RetryException()
1474
    except BaseException, e:
1475
        logger.exception(e)
1476
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1477
        if ctx:
1478
            ctx.mark_rollback()
1479 1501
    else:
1480 1502
        email = escape(m.person.email)
1481 1503
        msg = _(astakos_messages.USER_MEMBERSHIP_REMOVED) % email
1482 1504
        messages.success(request, msg)
1483
    return redirect(reverse('project_detail', args=(chain_id,)))
1505

  
1484 1506

  
1485 1507
@require_http_methods(["POST"])
1486 1508
@valid_astakos_user_required
1487
@project_transaction_context()
1488
def project_reject_member(request, chain_id, user_id, ctx=None):
1509
def project_reject_member(request, chain_id, user_id):
1510

  
1511
    with ExceptionHandler(request):
1512
        _project_reject_member(request, chain_id, user_id)
1513

  
1514
    return redirect(reverse('project_detail', args=(chain_id,)))
1515

  
1516

  
1517
@commit_on_success_strict()
1518
def _project_reject_member(request, chain_id, user_id):
1489 1519
    try:
1490 1520
        chain_id = int(chain_id)
1491 1521
        user_id = int(user_id)
1492 1522
        m = reject_membership(chain_id, user_id, request.user)
1493 1523
    except (IOError, PermissionDenied), e:
1494 1524
        messages.error(request, e)
1495
    except PendingMembershipError as e:
1496
        raise RetryException()
1497
    except BaseException, e:
1498
        logger.exception(e)
1499
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
1500
        if ctx:
1501
            ctx.mark_rollback()
1502 1525
    else:
1503 1526
        email = escape(m.person.email)
1504 1527
        msg = _(astakos_messages.USER_MEMBERSHIP_REJECTED) % email
1505 1528
        messages.success(request, msg)
1506
    return redirect(reverse('project_detail', args=(chain_id,)))
1529

  
1507 1530

  
1508 1531
@require_http_methods(["POST"])
1509 1532
@signed_terms_required
1510 1533
@login_required
1511
@project_transaction_context(sync=True)
1512
def project_app_approve(request, application_id, ctx=None):
1534
def project_app_approve(request, application_id):
1513 1535

  
1514 1536
    if not request.user.is_project_admin():
1515 1537
        m = _(astakos_messages.NOT_ALLOWED)
......
1520 1542
    except ProjectApplication.DoesNotExist:
1521 1543
        raise Http404
1522 1544

  
1523
    approve_application(application_id)
1545
    with ExceptionHandler(request):
1546
        _project_app_approve(request, application_id)
1547

  
1524 1548
    chain_id = get_related_project_id(application_id)
1525 1549
    return redirect(reverse('project_detail', args=(chain_id,)))
1526 1550

  
1551

  
1552
@commit_on_success_strict()
1553
def _project_app_approve(request, application_id):
1554
    approve_application(application_id)
1555

  
1556

  
1527 1557
@require_http_methods(["POST"])
1528 1558
@signed_terms_required
1529 1559
@login_required
1530
@project_transaction_context()
1531
def project_app_deny(request, application_id, ctx=None):
1560
def project_app_deny(request, application_id):
1532 1561

  
1533 1562
    reason = request.POST.get('reason', None)
1534 1563
    if not reason:
......
1543 1572
    except ProjectApplication.DoesNotExist:
1544 1573
        raise Http404
1545 1574

  
1546
    deny_application(application_id, reason=reason)
1575
    with ExceptionHandler(request):
1576
        _project_app_deny(request, application_id, reason)
1577

  
1547 1578
    return redirect(reverse('project_list'))
1548 1579

  
1580

  
1581
@commit_on_success_strict()
1582
def _project_app_deny(request, application_id, reason):
1583
    deny_application(application_id, reason=reason)
1584

  
1585

  
1549 1586
@require_http_methods(["POST"])
1550 1587
@signed_terms_required
1551 1588
@login_required
1552
@project_transaction_context()
1553
def project_app_dismiss(request, application_id, ctx=None):
1589
def project_app_dismiss(request, application_id):
1554 1590
    try:
1555 1591
        app = ProjectApplication.objects.get(id=application_id)
1556 1592
    except ProjectApplication.DoesNotExist:
......
1560 1596
        m = _(astakos_messages.NOT_ALLOWED)
1561 1597
        raise PermissionDenied(m)
1562 1598

  
1563
    # XXX: dismiss application also does authorization
1564
    dismiss_application(application_id, request_user=request.user)
1599
    with ExceptionHandler(request):
1600
        _project_app_dismiss(request, application_id)
1565 1601

  
1566 1602
    chain_id = None
1567 1603
    chain_id = get_related_project_id(application_id)
......
1571 1607
        next = reverse('project_list')
1572 1608
    return redirect(next)
1573 1609

  
1610

  
1611
def _project_app_dismiss(request, application_id):
1612
    # XXX: dismiss application also does authorization
1613
    dismiss_application(application_id, request_user=request.user)
1614

  
1615

  
1574 1616
@require_http_methods(["GET"])
1575 1617
@required_auth_methods_assigned(allow_access=True)
1576 1618
@login_required
b/snf-common/synnefo/lib/db/transaction.py
1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34

  
35
from django.db import transaction
36
import logging
37

  
38
logger = logging.getLogger(__name__)
39

  
40

  
41
def commit_on_success_strict(**kwargs):
42
    def wrap(func):
43
        @transaction.commit_manually(**kwargs)
44
        def inner(*args, **kwargs):
45
            try:
46
                result = func(*args, **kwargs)
47
                transaction.commit()
48
                return result
49
            except BaseException as e:
50
                logger.exception(e)
51
                transaction.rollback()
52
                raise
53
        return inner
54
    return wrap
/dev/null
1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

  
34
from django.db import transaction
35

  
36
# USAGE
37
# =====
38
# @transaction_context()
39
# def a_view(args, ctx=None):
40
#     ...
41
#     if ctx:
42
#         ctx.mark_rollback()
43
#     ...
44
#     return http response
45
#
46
# OR (more cleanly)
47
#
48
# def a_view(args):
49
#     with transaction_context() as ctx:
50
#         ...
51
#         ctx.mark_rollback()
52
#         ...
53
#         return http response
54

  
55
def transaction_context(**kwargs):
56
    return TransactionHandler(ctx=TransactionContext, **kwargs)
57

  
58

  
59
class TransactionContext(object):
60
    def __init__(self, **kwargs):
61
        self._rollback = False
62

  
63
    def mark_rollback(self):
64
        self._rollback = True
65

  
66
    def is_marked_rollback(self):
67
        return self._rollback
68

  
69
    def postprocess(self):
70
        pass
71

  
72

  
73
class TransactionHandler(object):
74
    def __init__(self, ctx=None, allow_postprocess=True, using=None, **kwargs):
75
        self.using             = using
76
        self.db                = (using if using is not None
77
                                  else transaction.DEFAULT_DB_ALIAS)
78
        self.ctx_class         = ctx
79
        self.ctx_kwargs        = kwargs
80
        self.allow_postprocess = allow_postprocess
81

  
82
    def __call__(self, func):
83
        def wrap(*args, **kwargs):
84
            with self as ctx:
85
                kwargs['ctx'] = ctx
86
                return func(*args, **kwargs)
87
        return wrap
88

  
89
    def __enter__(self):
90
        db = self.db
91
        transaction.enter_transaction_management(using=db)
92
        transaction.managed(True, using=db)
93
        self.ctx = self.ctx_class(self.ctx_kwargs)
94
        return self.ctx
95

  
96
    def __exit__(self, type, value, traceback):
97
        db = self.db
98
        trigger_postprocess = False
99
        try:
100
            if value is not None: # exception
101
                if transaction.is_dirty(using=db) or True:
102
                    # Rollback, even if is not dirty.
103
                    # This is a temporary bug fix for
104
                    # https://code.djangoproject.com/ticket/9964 .
105
                    # Django prior to 1.3 does not set a transaction
106
                    # dirty when the DB throws an exception, and thus
107
                    # does not trigger rollback, resulting in a
108
                    # dangling aborted DB transaction.
109
                    transaction.rollback(using=db)
110
            else:
111
                if transaction.is_dirty(using=db):
112
                    if self.ctx.is_marked_rollback():
113
                        transaction.rollback(using=db)
114
                    else:
115
                        try:
116
                            transaction.commit(using=db)
117
                        except:
118
                            transaction.rollback(using=db)
119
                            raise
120
                        else:
121
                            trigger_postprocess = True
122

  
123
                # postprocess,
124
                # even if there was nothing to commit
125
                # as long as it's not marked for rollback
126
                elif not self.ctx.is_marked_rollback():
127
                    trigger_postprocess = True
128
        finally:
129
            transaction.leave_transaction_management(using=db)
130

  
131
            # checking allow_postprocess is needed
132
            # in order to avoid endless recursion
133
            if trigger_postprocess and self.allow_postprocess:
134
                with TransactionHandler(ctx=self.ctx_class,
135
                                        allow_postprocess=False,
136
                                        using=self.using,
137
                                        **self.ctx_kwargs):
138
                    self.ctx.postprocess()

Also available in: Unified diff