Skip to content

Commit

Permalink
Merge dc340dc into 9543c8b
Browse files Browse the repository at this point in the history
  • Loading branch information
vangheem committed Mar 7, 2018
2 parents 9543c8b + dc340dc commit e348b5c
Show file tree
Hide file tree
Showing 20 changed files with 294 additions and 160 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
2.3.34 (unreleased)
-------------------

- Nothing changed yet.
- Be able to provide custom responses for unhandled exceptions
[vangheem]


2.3.33 (2018-03-03)
Expand Down
25 changes: 25 additions & 0 deletions docs/source/developer/exceptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Persistence

Exceptions during the rendering of API calls are wrapped, logged and provided
generic http status codes by default.

Guillotina provides a mechanism for customizing the status codes and type of
responses given depending on the exception type.

## Custom exception response

```python
from aiohttp.web_exceptions import HTTPPreconditionFailed
from guillotina import configure
from guillotina.interfaces import IErrorResponseException

import json


@configure.adapter(
for_=json.decoder.JSONDecodeError,
provides=IErrorResponseException)
def json_decode_error_response(exc, error='', eid=None):
return HTTPPreconditionFailed(
reason=f'JSONDecodeError: {eid}')
```
1 change: 1 addition & 0 deletions docs/source/developer/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ Contents:
persistence
blob
router
exceptions
api/index
13 changes: 9 additions & 4 deletions guillotina/api/addons.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from guillotina import configure
from guillotina import error_reasons
from guillotina._settings import app_settings
from guillotina.browser import ErrorResponse
from guillotina.i18n import MessageFactory
Expand Down Expand Up @@ -27,15 +28,17 @@ async def install(context, request):
if id_to_install not in app_settings['available_addons']:
return ErrorResponse(
'RequiredParam',
_("Property 'id' is required to be valid"))
_("Property 'id' is required to be valid"),
status=412, reason=error_reasons.INVALID_ID)

registry = request.container_settings
config = registry.for_interface(IAddons)

if id_to_install in config['enabled']:
return ErrorResponse(
'Duplicate',
_("Addon already installed"))
_("Addon already installed"),
status=412, reason=error_reasons.ALREADY_INSTALLED)
handler = app_settings['available_addons'][id_to_install]['handler']
await apply_coroutine(handler.install, context, request)
config['enabled'] |= {id_to_install}
Expand All @@ -59,15 +62,17 @@ async def uninstall(context, request):
if id_to_install not in app_settings['available_addons']:
return ErrorResponse(
'RequiredParam',
_("Property 'id' is required to be valid"))
_("Property 'id' is required to be valid"),
status=412, reason=error_reasons.INVALID_ID)

registry = request.container_settings
config = registry.for_interface(IAddons)

if id_to_install not in config['enabled']:
return ErrorResponse(
'Duplicate',
_("Addon not installed"))
_("Addon not installed"),
status=412, reason=error_reasons.NOT_INSTALLED)

handler = app_settings['available_addons'][id_to_install]['handler']
await apply_coroutine(handler.uninstall, context, request)
Expand Down
100 changes: 37 additions & 63 deletions guillotina/api/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from aiohttp.web_exceptions import HTTPNotFound
from aiohttp.web_exceptions import HTTPUnauthorized
from guillotina import configure
from guillotina import error_reasons
from guillotina import security
from guillotina._cache import FACTORY_CACHE
from guillotina._settings import app_settings
Expand All @@ -27,8 +28,6 @@
from guillotina.events import ObjectPermissionsViewEvent
from guillotina.events import ObjectRemovedEvent
from guillotina.events import ObjectVisitedEvent
from guillotina.exceptions import ConflictIdOnContainer
from guillotina.exceptions import NotAllowedContentType
from guillotina.exceptions import PreconditionFailed
from guillotina.i18n import default_message_factory as _
from guillotina.interfaces import IAbsoluteURL
Expand All @@ -47,7 +46,6 @@
from guillotina.interfaces import IResourceSerializeToJson
from guillotina.interfaces import IRolePermissionManager
from guillotina.interfaces import IRolePermissionMap
from guillotina.json.exceptions import DeserializationError
from guillotina.json.utils import convert_interfaces_to_schema
from guillotina.profile import profilable
from guillotina.transactions import get_transaction
Expand Down Expand Up @@ -159,15 +157,17 @@ async def __call__(self):
if not type_:
return ErrorResponse(
'RequiredParam',
_("Property '@type' is required"))
_("Property '@type' is required"),
reason=error_reasons.REQUIRED_PARAM_MISSING,
status=412)

# Generate a temporary id if the id is not given
if not id_:
new_id = None
else:
if not isinstance(id_, str) or not valid_id(id_):
return ErrorResponse('PreconditionFailed', str('Invalid id'),
status=412)
status=412, reason=error_reasons.INVALID_ID)
new_id = id_

user = get_authenticated_user_id(self.request)
Expand All @@ -177,16 +177,6 @@ async def __call__(self):
obj = await create_content_in_container(
self.context, type_, new_id, id=new_id, creators=(user,),
contributors=(user,))
except (PreconditionFailed, NotAllowedContentType) as e:
return ErrorResponse(
'PreconditionFailed',
str(e),
status=412)
except ConflictIdOnContainer as e:
return ErrorResponse(
'ConflictId',
str(e),
status=409)
except ValueError as e:
return ErrorResponse(
'CreatingObject',
Expand All @@ -203,16 +193,10 @@ async def __call__(self):
return ErrorResponse(
'DeserializationError',
'Cannot deserialize type {}'.format(obj.type_name),
status=412)
status=412,
reason=error_reasons.DESERIALIZATION_FAILED)

try:
await deserializer(data, validate_all=True)
except DeserializationError as e:
return ErrorResponse(
'DeserializationError',
str(e),
exc=e,
status=412)
await deserializer(data, validate_all=True)

# Local Roles assign owner as the creator user
get_owner = get_utility(IGetOwner)
Expand Down Expand Up @@ -259,7 +243,8 @@ async def __call__(self):
return ErrorResponse(
'DeserializationError',
'Not allowed to change id of content.',
status=412)
status=412,
reason=error_reasons.ID_NOT_ALLOWED)

behaviors = data.get('@behaviors', None)
for behavior in behaviors or ():
Expand All @@ -271,15 +256,10 @@ async def __call__(self):
return ErrorResponse(
'DeserializationError',
'Cannot deserialize type {}'.format(self.context.type_name),
status=412)
status=412,
reason=error_reasons.DESERIALIZATION_FAILED)

try:
await deserializer(data)
except DeserializationError as e:
return ErrorResponse(
'DeserializationError',
str(e),
status=422)
await deserializer(data)

await notify(ObjectModifiedEvent(self.context, payload=data))

Expand Down Expand Up @@ -392,25 +372,29 @@ async def __call__(self, changed=False):
if 'prinrole' not in data and \
'roleperm' not in data and \
'prinperm' not in data:
raise AttributeError('prinrole or roleperm or prinperm missing')
raise PreconditionFailed(
self.context, 'prinrole or roleperm or prinperm missing')

for prinrole in data.get('prinrole') or []:
setting = prinrole.get('setting')
if setting not in PermissionMap['prinrole']:
raise AttributeError('Invalid Type {}'.format(setting))
raise PreconditionFailed(
self.context, 'Invalid Type {}'.format(setting))
manager = IPrincipalRoleManager(context)
operation = PermissionMap['prinrole'][setting]
func = getattr(manager, operation)
if prinrole['role'] in lroles:
changed = True
func(prinrole['role'], prinrole['principal'])
else:
raise KeyError('No valid local role')
raise PreconditionFailed(
self.context, 'No valid local role')

for prinperm in data.get('prinperm') or []:
setting = prinperm['setting']
if setting not in PermissionMap['prinperm']:
raise AttributeError('Invalid Type')
raise PreconditionFailed(
self.context, 'Invalid Type')
manager = IPrincipalPermissionManager(context)
operation = PermissionMap['prinperm'][setting]
func = getattr(manager, operation)
Expand All @@ -420,7 +404,8 @@ async def __call__(self, changed=False):
for roleperm in data.get('roleperm') or []:
setting = roleperm['setting']
if setting not in PermissionMap['roleperm']:
raise AttributeError('Invalid Type')
raise PreconditionFailed(
self.context, 'Invalid Type')
manager = IRolePermissionManager(context)
operation = PermissionMap['roleperm'][setting]
func = getattr(manager, operation)
Expand Down Expand Up @@ -471,7 +456,7 @@ async def __call__(self):
})
async def can_i_do(context, request):
if 'permission' not in request.query:
raise TypeError('No permission param')
raise PreconditionFailed(context, 'No permission param')
permission = request.query['permission']
return IInteraction(request).check_permission(permission, context)

Expand Down Expand Up @@ -612,10 +597,7 @@ async def move(context, request):
destination_ob = None

if destination_ob is None:
return ErrorResponse(
'Configuration',
'Could not find destination object',
status=412)
raise PreconditionFailed(context, 'Could not find destination object')
old_id = context.id
if 'new_id' in data:
new_id = data['new_id']
Expand All @@ -625,16 +607,13 @@ async def move(context, request):

security = IInteraction(request)
if not security.check_permission('guillotina.AddContent', destination_ob):
return ErrorResponse(
'Configuration',
'You do not have permission to add content to the destination object',
status=412)
raise PreconditionFailed(
context, 'You do not have permission to add content to the '
'destination object')

if await destination_ob.async_contains(new_id):
return ErrorResponse(
'Configuration',
f'Destination already has object with the id {new_id}',
status=412)
raise PreconditionFailed(
context, f'Destination already has object with the id {new_id}')

original_parent = context.__parent__

Expand Down Expand Up @@ -699,27 +678,22 @@ async def duplicate(context, request):
if destination is not None:
destination_ob = await navigate_to(request.container, destination)
if destination_ob is None:
return ErrorResponse(
'Configuration',
'Could not find destination object',
status=412)
raise PreconditionFailed(
context, 'Could not find destination object',)
else:
destination_ob = context.__parent__

security = IInteraction(request)
if not security.check_permission('guillotina.AddContent', destination_ob):
return ErrorResponse(
'Configuration',
'You do not have permission to add content to the destination object',
status=412)
raise PreconditionFailed(
context, 'You do not have permission to add content to '
'the destination object',)

if 'new_id' in data:
new_id = data['new_id']
if await destination_ob.async_contains(new_id):
return ErrorResponse(
'Configuration',
f'Destination already has object with the id {new_id}',
status=412)
raise PreconditionFailed(
context, f'Destination already has object with the id {new_id}')
else:
count = 1
new_id = f'{context.id}-duplicate-{count}'
Expand Down
10 changes: 6 additions & 4 deletions guillotina/api/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from guillotina.browser import Response
from guillotina.component import get_adapter
from guillotina.exceptions import ComponentLookupError
from guillotina.exceptions import DeserializationError
from guillotina.i18n import MessageFactory
from guillotina.interfaces import IContainer
from guillotina.interfaces import IJSONToValue
from guillotina.interfaces import IRegistry
from guillotina.json.exceptions import DeserializationError
from guillotina.json.serialize_value import json_compatible
from guillotina.schema import get_fields
from guillotina.utils import import_class
Expand Down Expand Up @@ -108,15 +108,17 @@ async def __call__(self):
if not hasattr(self.request, 'container_settings'):
return ErrorResponse(
'BadRequest',
_("Not in a container request"))
_("Not in a container request"),
status=412)

data = await self.request.json()
interface = data.get('interface', None)
initial_values = data.get('initial_values', {})
if interface is None:
return ErrorResponse(
'InvalidRequest',
'Non existent Interface')
'Non existent Interface',
status=412)

registry = self.request.container_settings
iObject = import_class(interface)
Expand Down Expand Up @@ -170,7 +172,7 @@ async def publish_traverse(self, traverse):
async def __call__(self):
if self.key is _marker:
# No option to write the root of registry
return ErrorResponse('InvalidRequest', 'Needs the registry key')
return ErrorResponse('InvalidRequest', 'Needs the registry key', status=412)

data = await self.request.json()
if 'value' in data:
Expand Down

0 comments on commit e348b5c

Please sign in to comment.