Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add relationship repositories. #27

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
0.8.2 (unreleased)
------------------

- Nothing changed yet.
- Added relationship repositories.


0.8.1 (2018-07-03)
Expand Down
25 changes: 25 additions & 0 deletions flask_jsonapi/resource_repositories/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,28 @@ def get_count(self, filters=None):
@contextmanager
def begin_transaction(self):
yield


class ToOneRelationshipRepository:
def get_detail(self, parent_id):
raise exceptions.NotImplementedMethod('Getting relationship is not implemented.')

def update(self, parent_id, data):
raise exceptions.NotImplementedMethod('Updating relationship is not implemented.')

def delete(self, parent_id):
raise exceptions.NotImplementedMethod('Deleting relationship is not implemented.')


class ToManyRelationshipRepository:
def get_list(self, parent_id):
raise exceptions.NotImplementedMethod('Getting relationship is not implemented.')

def create(self, parent_id, data):
raise exceptions.NotImplementedMethod('Creating relationship is not implemented.')

def update(self, parent_id, data):
raise exceptions.NotImplementedMethod('Updating relationship is not implemented.')

def delete(self, parent_id, data):
raise exceptions.NotImplementedMethod('Deleting relationship is not implemented.')
182 changes: 176 additions & 6 deletions flask_jsonapi/resource_repositories/sqlalchemy_repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from flask_jsonapi import exceptions
from flask_jsonapi.resource_repositories import repositories
from flask_jsonapi.exceptions import ForbiddenError

logger = logging.getLogger(__name__)

Expand All @@ -25,7 +24,7 @@ def create(self, data, **kwargs):
return obj
except exc.SQLAlchemyError as error:
logger.exception(error)
raise ForbiddenError(detail='{} could not be created.'.format(self.instance_name.capitalize()))
raise exceptions.ForbiddenError(detail='{} could not be created.'.format(self.instance_name.capitalize()))

def get_list(self, filters=None, pagination=None):
try:
Expand All @@ -35,7 +34,7 @@ def get_list(self, filters=None, pagination=None):
return paginated_query.all()
except exc.SQLAlchemyError as error:
logger.exception(error)
raise ForbiddenError(detail='Error while getting {} list.'.format(self.instance_name))
raise exceptions.ForbiddenError(detail='Error while getting {} list.'.format(self.instance_name))

def get_detail(self, id):
try:
Expand All @@ -45,7 +44,7 @@ def get_detail(self, id):
detail='{} {} not found.'.format(self.instance_name.capitalize(), id))
except exc.SQLAlchemyError as error:
logger.exception(error)
raise ForbiddenError(detail='Error while getting {} details.'.format(self.instance_name))
raise exceptions.ForbiddenError(detail='Error while getting {} details.'.format(self.instance_name))

def delete(self, id):
obj = self.get_detail(id)
Expand All @@ -54,7 +53,7 @@ def delete(self, id):
self.session.flush()
except exc.SQLAlchemyError as error:
logger.exception(error)
raise ForbiddenError(detail='Error while deleting {}.'.format(self.instance_name))
raise exceptions.ForbiddenError(detail='Error while deleting {}.'.format(self.instance_name))

def update(self, data, **kwargs):
id = data['id']
Expand All @@ -66,7 +65,7 @@ def update(self, data, **kwargs):
return obj
except exc.SQLAlchemyError as error:
logger.exception(error)
raise ForbiddenError(detail='Error while updating {}.'.format(self.instance_name))
raise exceptions.ForbiddenError(detail='Error while updating {}.'.format(self.instance_name))

def get_query(self):
return self.model.query
Expand Down Expand Up @@ -101,3 +100,174 @@ def get_count(self, filters=None):
filtered_query = self.apply_filters(query, filters)
count_query = filtered_query.statement.with_only_columns([func.count()])
return self.session.execute(count_query).scalar()


class SqlAlchemyRelationshipRepositoryMixin:
session = None
parent_model_repository = None
related_model_repository = None
relationship_name = None
id_attribute = 'id'

@property
def _error_message(self):
return "Error while updating '{}' relationship.".format(self.relationship_name)

def _get_parent(self, parent_id):
parent = self.parent_model_repository.get_detail(parent_id)
return parent

def _get_current_related_objects(self, parent):
return getattr(parent, self.relationship_name)

def _flush(self):
try:
self.session.flush()
except exc.SQLAlchemyError as error:
logger.exception(error)
raise exceptions.ForbiddenError(detail=self._error_message)


class SqlAlchemyToOneRelationshipRepository(SqlAlchemyRelationshipRepositoryMixin,
repositories.ToOneRelationshipRepository):
def get_detail(self, parent_id):
parent = self._get_parent(parent_id)
object_ = self._get_current_related_objects(parent)
assert not isinstance(object_, list)
return object_

def update(self, parent_id, data):
assert not isinstance(data, list)
parent = self._get_parent(parent_id)
object_ = self.related_model_repository.get_detail(data[self.id_attribute])
self._update_relationship(parent, object_)
self._flush()
return object_

def delete(self, parent_id):
parent = self._get_parent(parent_id)
self._update_relationship(parent, None)
self._flush()
return None

def _update_relationship(self, parent, object_):
setattr(parent, self.relationship_name, object_)


class SqlAlchemyToManyRelationshipRepository(SqlAlchemyRelationshipRepositoryMixin,
repositories.ToManyRelationshipRepository):
def get_list(self, parent_id):
parent = self._get_parent(parent_id)
objects = self._get_current_related_objects(parent)
assert isinstance(objects, list)
return objects

def create(self, parent_id, data):
assert isinstance(data, list)
parent = self._get_parent(parent_id)
objects_ids = self._get_objects_ids(data)
self._add_to_relationship(parent, objects_ids)
self._flush()
return self._get_current_related_objects(parent)

def update(self, parent_id, data):
assert isinstance(data, list)
parent = self._get_parent(parent_id)
objects_ids = self._get_objects_ids(data)

self._add_to_relationship(parent, objects_ids)

objects_ids_to_delete = self._get_objects_ids_to_delete_in_update(parent, objects_ids)
self._delete_from_relationship(parent, objects_ids_to_delete)

self._flush()
return self._get_current_related_objects(parent)

def delete(self, parent_id, data):
assert isinstance(data, list)
parent = self._get_parent(parent_id)
objects_ids = self._get_objects_ids(data)
self._delete_from_relationship(parent, objects_ids)
self._flush()
return self._get_current_related_objects(parent)

def _get_objects_ids(self, data):
return [object_[self.id_attribute] for object_ in data]

def _add_to_relationship(self, parent, objects_ids):
objects_to_add = self._get_objects_to_add(parent, objects_ids)
current_related_objects = self._get_current_related_objects(parent)
current_related_objects.extend(objects_to_add)

def _get_objects_to_add(self, parent, objects_ids):
current_related_objects_ids = self._get_current_related_objects_ids(parent)
objects_ids_to_add = list(set(objects_ids) - set(current_related_objects_ids))
objects_to_add = self._get_objects(objects_ids_to_add)
return objects_to_add

def _get_current_related_objects_ids(self, parent):
return [getattr(object_, self.id_attribute) for object_ in self._get_current_related_objects(parent)]

def _get_objects(self, objects_ids):
return [self.related_model_repository.get_detail(id) for id in objects_ids]

def _get_objects_ids_to_delete_in_update(self, parent, objects_ids):
current_related_objects_ids = self._get_current_related_objects_ids(parent)
objects_ids_to_delete = list(set(current_related_objects_ids) - set(objects_ids))
return objects_ids_to_delete

def _delete_from_relationship(self, parent, objects_ids):
objects_to_delete = self._get_objects_to_delete(parent, objects_ids)
self._delete_objects_from_relationship(parent, objects_to_delete)

def _get_objects_to_delete(self, parent, objects_ids):
current_related_objects_ids = self._get_current_related_objects_ids(parent)
objects_ids_to_delete = list(set(objects_ids) & set(current_related_objects_ids))
if len(objects_ids) != len(objects_ids_to_delete):
raise exceptions.ForbiddenError(detail=self._error_message)
objects_to_delete = self._get_objects(objects_ids_to_delete)
return objects_to_delete

def _delete_objects_from_relationship(self, parent, objects_to_delete):
current_related_objects = self._get_current_related_objects(parent)
for object_ in objects_to_delete:
current_related_objects.remove(object_)


class SqlAlchemyAssociationRepository(SqlAlchemyToManyRelationshipRepository):
association_model = None
parent_id_attribute = None

def get_query(self):
return self.association_model.query

def _get_objects_to_add(self, parent, objects_ids):
current_related_objects_ids = self._get_current_related_objects_ids(parent)
objects_ids_to_add = list(set(objects_ids) - set(current_related_objects_ids))
objects_to_add = self._create_objects(objects_ids_to_add)
return objects_to_add

def _create_objects(self, objects_ids_to_add):
return [self._build_object({self.id_attribute: id}) for id in objects_ids_to_add]

def _build_object(self, kwargs):
return self.association_model(**kwargs)

def _get_objects_to_delete(self, parent, objects_ids):
current_related_objects_ids = self._get_current_related_objects_ids(parent)
objects_ids_to_delete = list(set(objects_ids) & set(current_related_objects_ids))
if len(objects_ids) != len(objects_ids_to_delete):
raise exceptions.ForbiddenError(detail=self._error_message)
objects_to_delete = self._get_objects(parent, objects_ids_to_delete)
return objects_to_delete

def _get_objects(self, parent, objects_ids):
id_attribute_orm_field = getattr(self.association_model, self.id_attribute)
parent_id_attribute_orm_field = getattr(self.association_model, self.parent_id_attribute)
objects = (self.get_query()
.filter(id_attribute_orm_field.in_(objects_ids))
.filter(parent_id_attribute_orm_field == parent.id)
.all())
if len(objects) != len(objects_ids):
raise exceptions.ForbiddenError(detail=self._error_message)
return objects
36 changes: 36 additions & 0 deletions flask_jsonapi/resource_repository_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,39 @@ def get_views_kwargs(self):
'repository': self.repository,
**(self.view_kwargs or {})
}


class RelationshipRepositoryViewMixin:
def __init__(self, *, repository=None, **kwargs):
super().__init__(**kwargs)
if repository:
self.repository = repository


class ToOneRelationshipRepositoryView(RelationshipRepositoryViewMixin, resources.ToOneRelationship):
repository = repositories.ToOneRelationshipRepository()

def read(self, parent_id):
return self.repository.get_detail(parent_id)

def update(self, parent_id, data):
return self.repository.update(parent_id, data)

def destroy(self, parent_id):
return self.repository.delete(parent_id)


class ToManyRelationshipRepositoryView(RelationshipRepositoryViewMixin, resources.ToManyRelationship):
repository = repositories.ToManyRelationshipRepository()

def read(self, parent_id):
return self.repository.get_list(parent_id)

def create(self, parent_id, data):
return self.repository.create(parent_id, data)

def update(self, parent_id, data):
return self.repository.update(parent_id, data)

def destroy(self, parent_id, data):
return self.repository.delete(parent_id, data)
Loading