Skip to content

Commit

Permalink
Fixes #367: catch IntegrityErrors in all methods.
Browse files Browse the repository at this point in the history
  • Loading branch information
jfinkels committed Oct 30, 2014
1 parent c4a25b5 commit e4f802d
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 8 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ Version 0.15.1-dev

Not yet released.

- #367: catch :exc:`IntegrityError`, :exc:`DataError`, and
:exc:`ProgrammingError` exceptions in all view methods.

Version 0.15.0
--------------

Expand Down
49 changes: 41 additions & 8 deletions flask_restless/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,38 @@ def decorator(*args, **kw):
return decorator


def catch_integrity_errors(session):
"""Returns a decorator that catches database integrity errors.
`session` is the SQLAlchemy session in which all database transactions will
be performed.
View methods can be wrapped like this::
@catch_integrity_errors(session)
def get(self, *args, **kw):
return '...'
Specifically, functions wrapped with the returned decorator catch
:exc:`IntegrityError`s, :exc:`DataError`s, and
:exc:`ProgrammingError`s. After the exceptions are caught, the session is
rolled back, the exception is logged on the current Flask application, and
an error response is returned to the client.
"""
def decorator(func):
@wraps(func)
def wrapped(*args, **kw):
try:
return func(*args, **kw)
except (DataError, IntegrityError, ProgrammingError) as exception:
session.rollback()
current_app.logger.exception(str(exception))
return dict(message=type(exception).__name__), 400
return wrapped
return decorator


def set_headers(response, headers):
"""Sets the specified headers on the specified response.
Expand Down Expand Up @@ -610,6 +642,15 @@ class is an API. This model should live in `database`.
for preprocessor in self.preprocessors['PUT_MANY']:
self.preprocessors['PATCH_MANY'].append(preprocessor)

# HACK: We would like to use the :attr:`API.decorators` class attribute
# in order to decorate each view method with a decorator that catches
# database integrity errors. However, in order to rollback the session,
# we need to have a session object available to roll back. Therefore we
# need to manually decorate each of the view functions here.
decorate = lambda name, f: setattr(self, name, f(getattr(self, name)))
for method in ['get', 'post', 'patch', 'put', 'delete']:
decorate(method, catch_integrity_errors(self.session))

def _get_column_name(self, column):
"""Retrieve a column name from a column attribute of SQLAlchemy
model class, or a string.
Expand Down Expand Up @@ -1285,10 +1326,6 @@ def post(self):
return result, 201, headers
except self.validation_exceptions as exception:
return self._handle_validation_exception(exception)
except (DataError, IntegrityError, ProgrammingError) as exception:
self.session.rollback()
current_app.logger.exception(str(exception))
return dict(message=type(exception).__name__), 400

def patch(self, instid, relationname, relationinstid):
"""Updates the instance specified by ``instid`` of the named model, or
Expand Down Expand Up @@ -1398,10 +1435,6 @@ def patch(self, instid, relationname, relationinstid):
except self.validation_exceptions as exception:
current_app.logger.exception(str(exception))
return self._handle_validation_exception(exception)
except (DataError, IntegrityError, ProgrammingError) as exception:
self.session.rollback()
current_app.logger.exception(str(exception))
return dict(message=type(exception).__name__), 400

# Perform any necessary postprocessing.
if patchmany:
Expand Down
9 changes: 9 additions & 0 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,15 @@ def test_delete(self):
people = self.session.query(self.Person).filter_by(id=1)
assert people.count() == 0

def test_delete_integrity_error(self):
"""Tests that an :exc:`IntegrityError` raised in a
:http:method:`delete` request is caught and returned to the client
safely.
"""
# TODO Fill me in.
pass

def test_delete_absent_instance(self):
"""Test that deleting an instance of the model which does not exist
fails.
Expand Down

0 comments on commit e4f802d

Please sign in to comment.