Skip to content

Commit

Permalink
Merge pull request ColtonProvias#67 from duk3luk3/fix-43-post-collection
Browse files Browse the repository at this point in the history
  • Loading branch information
ColtonProvias authored Jan 12, 2018
2 parents ca983ac + 6defe78 commit 40f8b59
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 14 deletions.
12 changes: 7 additions & 5 deletions docs/models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,25 +49,27 @@ Relationship's come in two flavors: to-one and to-many (or tranditional and
LDS-flavored if you prefer those terms). To one descriptors have the actions
GET and SET::

@rel_descriptor(RelationshipActions.GET, 'significant_other')
from sqlalchemy_jsonapi import relationship_descriptor, RelationshipActions

@relationship_descriptor(RelationshipActions.GET, 'significant_other')
def getter(self):
# ...

@rel_descriptor(RelationshipActions.SET, 'significant_other')
@relationship_descriptor(RelationshipActions.SET, 'significant_other')
def setter(self, value):
# ...

To-many have GET, APPEND, and DELETE::

@rel_descriptor(RelationshipActions.GET, 'angry_exes')
@relationship_descriptor(RelationshipActions.GET, 'angry_exes')
def getter(self):
# ...

@rel_descriptor(RelationshipActions.APPEND, 'angry_exes')
@relationship_descriptor(RelationshipActions.APPEND, 'angry_exes')
def appender(self):
# ...

@rel_descriptor(RelationshipActions.DELETE, 'angry_exes')
@relationship_descriptor(RelationshipActions.DELETE, 'angry_exes')
def remover(self):
# ...

Expand Down
18 changes: 12 additions & 6 deletions sqlalchemy_jsonapi/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ class Permissions(Enum):
EDIT = 102
DELETE = 103

class MissingKey:
def __init__(self, elem):
self.elem = elem

def __repr__(self):
return '<{} elem={}>'.format(self.__class__.__name__, self.elem)

ALL_PERMISSIONS = {
Permissions.VIEW, Permissions.CREATE, Permissions.EDIT, Permissions.DELETE
Expand Down Expand Up @@ -266,7 +272,7 @@ def __init__(self, base, prefix=''):
}
rels_desc = model.__jsonapi_rel_desc__
for relationship in prop_value.__jsonapi_desc_for_rels__:
rels_desc.setdefault(attribute, defaults)
rels_desc.setdefault(relationship, defaults)
rel_desc = rels_desc[relationship]
for action in prop_value.__jsonapi_action__:
rel_desc[action] = prop_value
Expand Down Expand Up @@ -980,15 +986,15 @@ def post_collection(self, session, data, api_type):
data['data'].setdefault('attributes', {})

data_keys = set(map((
lambda x: resource.__jsonapi_map_to_py__.get(x, None)),
lambda x: resource.__jsonapi_map_to_py__.get(x, MissingKey(x))),
data['data'].get('relationships', {}).keys()))
model_keys = set(resource.__mapper__.relationships.keys())
if not data_keys <= model_keys:
data_keys = set([key.elem if isinstance(key, MissingKey) else key for key in data_keys])
# pragma: no cover
raise BadRequestError(
'{} not relationships for {}'.format(
', '.join(list(data_keys -
model_keys)), model.__jsonapi_type__))
'{} not relationships for {}'.format(
', '.join([repr(key) for key in list(data_keys - model_keys)]), model.__jsonapi_type__))

attrs_to_ignore = {'__mapper__', 'id'}

Expand Down Expand Up @@ -1043,7 +1049,7 @@ def post_collection(self, session, data, api_type):
raise BadRequestError(
'{} must be an array'.format(key))
for item in data_rel:
if not {'type', 'id'} in set(item.keys()):
if 'type' not in item.keys() or 'id' not in item.keys():
raise BadRequestError(
'{} must have type and id keys'.format(key))
# pragma: no cover
Expand Down
29 changes: 28 additions & 1 deletion sqlalchemy_jsonapi/tests/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@

from flask import Flask, request
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Boolean, Column, ForeignKey, Unicode, UnicodeText
from sqlalchemy import Boolean, Column, ForeignKey, Unicode, UnicodeText, Table
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import backref, relationship, validates
from sqlalchemy_jsonapi import (
FlaskJSONAPI, Permissions, permission_test, Method, Endpoint,
relationship_descriptor, RelationshipActions,
INTERACTIVE_PERMISSIONS)
from sqlalchemy_utils import EmailType, PasswordType, Timestamp, UUIDType

Expand Down Expand Up @@ -91,6 +92,10 @@ def allow_delete(self):
""" Just like a popular social media site, we won't delete users. """
return False

PostTags = Table('post_tag', db.Model.metadata,
Column('post_id', UUIDType, ForeignKey('posts.id')),
Column('tag_id', UUIDType, ForeignKey('tags.id'))
)

class BlogPost(Timestamp, db.Model):
"""Post model, as if this is a blog."""
Expand All @@ -109,6 +114,10 @@ class BlogPost(Timestamp, db.Model):
backref=backref('posts',
lazy='dynamic'))

tags = relationship("BlogTag",
secondary=PostTags,
back_populates="posts")

@validates('title')
def validate_title(self, key, title):
"""Keep titles from getting too long."""
Expand All @@ -125,6 +134,18 @@ def allow_view(self):
def prevent_altering_of_logs(self):
return False

class BlogTag(Timestamp, db.Model):
"""Blogs can have tags now"""

__tablename__ = 'tags'

id = Column(UUIDType, default=uuid4, primary_key=True)
slug = Column(Unicode(100), unique=True)
description = Column(UnicodeText)

posts = relationship("BlogPost",
secondary=PostTags,
back_populates="tags")

class BlogComment(Timestamp, db.Model):
"""Comment for each Post."""
Expand All @@ -140,6 +161,12 @@ class BlogComment(Timestamp, db.Model):
lazy='joined',
backref=backref('comments',
lazy='dynamic'))

@relationship_descriptor(RelationshipActions.GET, 'post')
def post_get(self):
"""No-OP Relationship descriptor to exercise relationship_descriptor"""
return self.post

author = relationship('User',
lazy='joined',
backref=backref('comments',
Expand Down
9 changes: 8 additions & 1 deletion sqlalchemy_jsonapi/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from flask.testing import FlaskClient
from sqlalchemy.orm import sessionmaker
from app import db as db_
from app import app, User, BlogPost, BlogComment, Log
from app import app, User, BlogPost, BlogComment, BlogTag, Log
from faker import Faker

Session = sessionmaker()
Expand Down Expand Up @@ -116,6 +116,13 @@ def bunch_of_posts(user, session):
BlogComment(author=user, content=fake.paragraph()))
session.commit()

@pytest.fixture
def bunch_of_tags(session):
tags = [BlogTag(slug=fake.word(), description=fake.text()) for x in range(3)]
for tag in tags:
session.add(tag)
session.commit()
return tags

@pytest.fixture
def comment(user, post, session):
Expand Down
64 changes: 63 additions & 1 deletion sqlalchemy_jsonapi/tests/test_collection_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from sqlalchemy_jsonapi.errors import (
InvalidTypeForEndpointError, MissingTypeError, PermissionDeniedError,
ValidationError, MissingContentTypeError)
ValidationError, MissingContentTypeError, BadRequestError)
from faker import Faker

fake = Faker()
Expand Down Expand Up @@ -57,6 +57,43 @@ def test_200_resource_creation_with_relationships(user, client):
'id'
] == str(user.id)

def test_200_resource_creation_with_relationship_array(user, bunch_of_tags, client):
payload = {
'data': {
'type': 'blog-posts',
'attributes': {
'title': 'Some title',
'content': 'Hello, World!',
'is-published': True
},
'relationships': {
'author': {
'data': {
'type': 'users',
'id': str(user.id)
}
},
'tags': {
'data': [{
'type': 'blog-tags',
'id': str(tag.id)
} for tag in bunch_of_tags
]
}
}
}
}
response = client.post(
'/api/blog-posts/', data=json.dumps(payload),
content_type='application/vnd.api+json').validate(201)
assert response.json_data['data']['type'] == 'blog-posts'
post_id = response.json_data['data']['id']
response = client.get(
'/api/blog-posts/{}/?include=author'.format(post_id)).validate(200)
assert response.json_data['data']['relationships']['author']['data'][
'id'
] == str(user.id)


def test_403_when_access_is_denied(client):
payload = {'data': {'type': 'logs'}}
Expand Down Expand Up @@ -146,3 +183,28 @@ def test_409_for_wrong_field_name(client):
'/api/users/', data=json.dumps(payload),
content_type='application/vnd.api+json').validate(
409, ValidationError)


def test_400_for_unknown_relationship_type(user, client):
payload = {
'data': {
'type': 'blog-posts',
'attributes': {
'title': 'Some title',
'content': 'Hello, World!',
'is-published': True
},
'relationships': {
'bogon': {
'data': {
'type': 'users',
'id': str(user.id)
}
}
}
}
}
client.post(
'/api/blog-posts/', data=json.dumps(payload),
content_type='application/vnd.api+json').validate(
400, BadRequestError)

0 comments on commit 40f8b59

Please sign in to comment.