Tighten Backend Patterns (#159)

This commit is contained in:
Daniel Ternyak 2018-10-22 17:31:33 -05:00 committed by GitHub
parent eae0e81ff0
commit 357b4248c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 97 additions and 138 deletions

View File

@ -1,72 +0,0 @@
import copy
import re
from flask import jsonify
def _camel_dict(dict_obj, deep=True):
converted_dict_obj = {}
for snake_case_k in dict_obj:
camel_case_k = re.sub('_([a-z])', lambda match: match.group(1).upper(), snake_case_k)
value = dict_obj[snake_case_k]
if type(value) == dict and deep:
converted_dict_obj[camel_case_k] = camel(**value)
elif type(value) == list and deep:
converted_list_items = []
for item in value:
converted_list_items.append(camel(**item))
converted_dict_obj[camel_case_k] = converted_list_items
else:
converted_dict_obj[camel_case_k] = dict_obj[snake_case_k]
return converted_dict_obj
def camel(dict_or_list_obj=None, **kwargs):
dict_or_list_obj = kwargs if kwargs else dict_or_list_obj
deep = True
if type(dict_or_list_obj) == dict:
return _camel_dict(dict_obj=dict_or_list_obj, deep=deep)
elif type(dict_or_list_obj) == list or type(dict_or_list_obj) == tuple or type(dict_or_list_obj) == map:
return list(map(_camel_dict, list(dict_or_list_obj)))
else:
raise ValueError("type {} is not supported!".format(type(dict_or_list_obj)))
"""
JSONResponse allows several argument formats:
1. JSONResponse([{"userId": 1, "name": "John" }, {"userId": 2, "name": "Dave" }])
2. JSONResponse(result=[my_results])
JSONResponse does not accept the following:
1. Intermixed positional and keyword arguments: JSONResponse(some_data, wow=True)
1a. The exception to this is _statusCode, which is allowed to be mixed.
An HTTP Status code should be set here by the caller, or 200 will be used.
1. Multiple positional arguments: JSONResponse(some_data, other_data)
"""
# TODO - use something standard. Insane that it's so hard to camelCase JSON output
def JSONResponse(*args, **kwargs):
if args:
if len(args) > 1:
raise ValueError("Only one positional arg supported")
if kwargs.get("_statusCode"):
status = copy.copy(kwargs["_statusCode"])
del kwargs["_statusCode"]
else:
status = 200
if args and kwargs:
raise ValueError("Only positional args or keyword args supported, not both")
if not kwargs and not args:
# TODO add log. This should never happen
return jsonify({}), 500
if kwargs:
return jsonify(camel(**kwargs)), status
else:
return jsonify(camel(args[0])), status

View File

@ -1,6 +1,5 @@
from flask import Blueprint from flask import Blueprint, jsonify
from grant import JSONResponse from animal_case import animalify
from .models import Comment, comments_schema from .models import Comment, comments_schema
@ -11,4 +10,4 @@ blueprint = Blueprint("comment", __name__, url_prefix="/api/v1/comment")
def get_comments(): def get_comments():
all_comments = Comment.query.all() all_comments = Comment.query.all()
result = comments_schema.dump(all_comments) result = comments_schema.dump(all_comments)
return JSONResponse(result) return jsonify(animalify(result))

View File

@ -1,6 +1,7 @@
from flask import Blueprint from flask import Blueprint, jsonify
from animal_case import animalify
from grant import JSONResponse
from .models import Milestone, milestones_schema from .models import Milestone, milestones_schema
blueprint = Blueprint('milestone', __name__, url_prefix='/api/v1/milestones') blueprint = Blueprint('milestone', __name__, url_prefix='/api/v1/milestones')
@ -10,4 +11,4 @@ blueprint = Blueprint('milestone', __name__, url_prefix='/api/v1/milestones')
def get_users(): def get_users():
milestones = Milestone.query.all() milestones = Milestone.query.all()
result = milestones_schema.dump(milestones) result = milestones_schema.dump(milestones)
return JSONResponse(result) return jsonify(animalify(result))

View File

@ -1,9 +1,10 @@
from datetime import datetime from datetime import datetime
from flask import Blueprint, request from animal_case import animalify
from flask import Blueprint, jsonify
from flask_yoloapi import endpoint, parameter
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from grant import JSONResponse
from grant.comment.models import Comment, comment_schema from grant.comment.models import Comment, comment_schema
from grant.milestone.models import Milestone from grant.milestone.models import Milestone
from grant.user.models import User, SocialMedia, Avatar from grant.user.models import User, SocialMedia, Avatar
@ -17,9 +18,9 @@ def get_proposal(proposal_id):
proposal = Proposal.query.filter_by(proposal_id=proposal_id).first() proposal = Proposal.query.filter_by(proposal_id=proposal_id).first()
if proposal: if proposal:
dumped_proposal = proposal_schema.dump(proposal) dumped_proposal = proposal_schema.dump(proposal)
return JSONResponse(dumped_proposal) return jsonify(animalify(dumped_proposal))
else: else:
return JSONResponse(message="No proposal matching id", _statusCode=404) return jsonify(message="No proposal matching id"), 404
@blueprint.route("/<proposal_id>/comments", methods=["GET"]) @blueprint.route("/<proposal_id>/comments", methods=["GET"])
@ -27,22 +28,23 @@ def get_proposal_comments(proposal_id):
proposal = Proposal.query.filter_by(proposal_id=proposal_id).first() proposal = Proposal.query.filter_by(proposal_id=proposal_id).first()
if proposal: if proposal:
dumped_proposal = proposal_schema.dump(proposal) dumped_proposal = proposal_schema.dump(proposal)
return JSONResponse( return jsonify(animalify(
proposal_id=proposal_id, proposal_id=proposal_id,
total_comments=len(dumped_proposal["comments"]), total_comments=len(dumped_proposal["comments"]),
comments=dumped_proposal["comments"] comments=dumped_proposal["comments"]
) ))
else: else:
return JSONResponse(message="No proposal matching id", _statusCode=404) return jsonify(message="No proposal matching id", _statusCode=404)
@blueprint.route("/<proposal_id>/comments", methods=["POST"]) @blueprint.route("/<proposal_id>/comments", methods=["POST"])
def post_proposal_comments(proposal_id): @endpoint.api(
parameter('userId', type=int, required=True),
parameter('content', type=str, required=True)
)
def post_proposal_comments(proposal_id, user_id, content):
proposal = Proposal.query.filter_by(proposal_id=proposal_id).first() proposal = Proposal.query.filter_by(proposal_id=proposal_id).first()
if proposal: if proposal:
incoming = request.get_json()
user_id = incoming["userId"]
content = incoming["content"]
user = User.query.filter_by(id=user_id).first() user = User.query.filter_by(id=user_id).first()
if user: if user:
@ -54,17 +56,19 @@ def post_proposal_comments(proposal_id):
db.session.add(comment) db.session.add(comment)
db.session.commit() db.session.commit()
dumped_comment = comment_schema.dump(comment) dumped_comment = comment_schema.dump(comment)
return JSONResponse(dumped_comment, _statusCode=201) return dumped_comment, 201
else: else:
return JSONResponse(message="No user matching id", _statusCode=404) return {"message": "No user matching id"}, 404
else: else:
return JSONResponse(message="No proposal matching id", _statusCode=404) return {"message": "No proposal matching id"}, 404
@blueprint.route("/", methods=["GET"]) @blueprint.route("/", methods=["GET"])
def get_proposals(): @endpoint.api(
stage = request.args.get("stage") parameter('stage', type=str, required=False)
)
def get_proposals(stage):
if stage: if stage:
proposals = ( proposals = (
Proposal.query.filter_by(stage=stage) Proposal.query.filter_by(stage=stage)
@ -74,24 +78,27 @@ def get_proposals():
else: else:
proposals = Proposal.query.order_by(Proposal.date_created.desc()).all() proposals = Proposal.query.order_by(Proposal.date_created.desc()).all()
dumped_proposals = proposals_schema.dump(proposals) dumped_proposals = proposals_schema.dump(proposals)
return JSONResponse(dumped_proposals) return dumped_proposals
@blueprint.route("/", methods=["POST"]) @blueprint.route("/", methods=["POST"])
def make_proposal(): @endpoint.api(
parameter('crowdFundContractAddress', type=str, required=True),
parameter('content', type=str, required=True),
parameter('title', type=str, required=True),
parameter('milestones', type=list, required=True),
parameter('category', type=str, required=True),
parameter('team', type=list, required=True)
)
def make_proposal(crowd_fund_contract_address, content, title, milestones, category, team):
from grant.user.models import User from grant.user.models import User
existing_proposal = Proposal.query.filter_by(proposal_id=crowd_fund_contract_address).first()
incoming = request.get_json() if existing_proposal:
return {"message": "Oops! Something went wrong."}, 409
proposal_id = incoming["crowdFundContractAddress"]
content = incoming["content"]
title = incoming["title"]
milestones = incoming["milestones"]
category = incoming["category"]
proposal = Proposal.create( proposal = Proposal.create(
stage="FUNDING_REQUIRED", stage="FUNDING_REQUIRED",
proposal_id=proposal_id, proposal_id=crowd_fund_contract_address,
content=content, content=content,
title=title, title=title,
category=category category=category
@ -99,9 +106,8 @@ def make_proposal():
db.session.add(proposal) db.session.add(proposal)
team = incoming["team"]
if not len(team) > 0: if not len(team) > 0:
return JSONResponse(message="Team must be at least 1", _statusCode=400) return {"message": "Team must be at least 1"}, 400
for team_member in team: for team_member in team:
account_address = team_member.get("accountAddress") account_address = team_member.get("accountAddress")
@ -149,7 +155,7 @@ def make_proposal():
db.session.commit() db.session.commit()
except IntegrityError as e: except IntegrityError as e:
print(e) print(e)
return JSONResponse(message="Proposal with that hash already exists", _statusCode=409) return {"message": "Oops! Something went wrong."}, 409
results = proposal_schema.dump(proposal) results = proposal_schema.dump(proposal)
return JSONResponse(results, _statusCode=201) return results, 201

View File

@ -1,32 +1,35 @@
from flask import Blueprint, request, g from animal_case import animalify
from flask import Blueprint, g, jsonify
from flask_yoloapi import endpoint, parameter
from grant import JSONResponse
from .models import User, users_schema, user_schema, db from .models import User, users_schema, user_schema, db
from ..email.send import send_email
from ..proposal.models import Proposal, proposal_team from ..proposal.models import Proposal, proposal_team
from ..utils.auth import requires_sm from ..utils.auth import requires_sm
from ..email.send import send_email
blueprint = Blueprint('user', __name__, url_prefix='/api/v1/users') blueprint = Blueprint('user', __name__, url_prefix='/api/v1/users')
@blueprint.route("/", methods=["GET"]) @blueprint.route("/", methods=["GET"])
def get_users(): @endpoint.api(
proposal_query = request.args.get('proposalId') parameter('proposalId', type=str, required=False)
proposal = Proposal.query.filter_by(proposal_id=proposal_query).first() )
def get_users(proposal_id):
proposal = Proposal.query.filter_by(proposal_id=proposal_id).first()
if not proposal: if not proposal:
users = User.query.all() users = User.query.all()
else: else:
users = User.query.join(proposal_team).join(Proposal) \ users = User.query.join(proposal_team).join(Proposal) \
.filter(proposal_team.c.proposal_id == proposal.id).all() .filter(proposal_team.c.proposal_id == proposal.id).all()
result = users_schema.dump(users) result = users_schema.dump(users)
return JSONResponse(result) return result
@blueprint.route("/me", methods=["GET"]) @blueprint.route("/me", methods=["GET"])
@requires_sm @requires_sm
def get_me(): def get_me():
dumped_user = user_schema.dump(g.current_user) dumped_user = user_schema.dump(g.current_user)
return JSONResponse(dumped_user) return jsonify(animalify(dumped_user))
@blueprint.route("/<user_identity>", methods=["GET"]) @blueprint.route("/<user_identity>", methods=["GET"])
@ -34,26 +37,23 @@ def get_user(user_identity):
user = User.get_by_email_or_account_address(email_address=user_identity, account_address=user_identity) user = User.get_by_email_or_account_address(email_address=user_identity, account_address=user_identity)
if user: if user:
result = user_schema.dump(user) result = user_schema.dump(user)
return JSONResponse(result) return jsonify(animalify(result))
else: else:
return JSONResponse( return jsonify(
message="User with account_address or user_identity matching {} not found".format(user_identity), message="User with account_address or user_identity matching {} not found".format(user_identity)), 404
_statusCode=404)
@blueprint.route("/", methods=["POST"]) @blueprint.route("/", methods=["POST"])
def create_user(): @endpoint.api(
incoming = request.get_json() parameter('accountAddress', type=str, required=True),
account_address = incoming["accountAddress"] parameter('emailAddress', type=str, required=True),
email_address = incoming["emailAddress"] parameter('displayName', type=str, required=True),
display_name = incoming["displayName"] parameter('title', type=str, required=True),
title = incoming["title"] )
def create_user(account_address, email_address, display_name, title):
existing_user = User.get_by_email_or_account_address(email_address=email_address, account_address=account_address) existing_user = User.get_by_email_or_account_address(email_address=email_address, account_address=account_address)
if existing_user: if existing_user:
return JSONResponse( return {"message": "User with that address or email already exists"}, 409
message="User with that address or email already exists",
_statusCode=400)
# TODO: Handle avatar & social stuff too # TODO: Handle avatar & social stuff too
user = User( user = User(
@ -73,4 +73,4 @@ def create_user():
}) })
result = user_schema.dump(user) result = user_schema.dump(user)
return JSONResponse(result) return result

View File

@ -16,4 +16,5 @@ flake8-quotes==1.0.0
isort==4.3.4 isort==4.3.4
pep8-naming==0.7.0 pep8-naming==0.7.0
pre-commit pre-commit
flask_testing flask_testing
mock

View File

@ -53,3 +53,6 @@ markdownify
# email # email
flask-sendgrid==0.6 flask-sendgrid==0.6
sendgrid==5.3.0 sendgrid==5.3.0
# input validation
flask-yolo2API

View File

@ -48,7 +48,7 @@ class TestAPI(BaseTestConfig):
proposal_id=proposal["crowdFundContractAddress"] proposal_id=proposal["crowdFundContractAddress"]
).first()) ).first())
self.app.post( resp = self.app.post(
"/api/v1/proposals/", "/api/v1/proposals/",
data=json.dumps(proposal), data=json.dumps(proposal),
content_type='application/json' content_type='application/json'
@ -92,3 +92,18 @@ class TestAPI(BaseTestConfig):
) )
self.assertTrue(comment_res.json) self.assertTrue(comment_res.json)
def test_create_new_proposal_duplicate(self):
proposal_res = self.app.post(
"/api/v1/proposals/",
data=json.dumps(proposal),
content_type='application/json'
)
proposal_res2 = self.app.post(
"/api/v1/proposals/",
data=json.dumps(proposal),
content_type='application/json'
)
self.assertEqual(proposal_res2.status_code, 409)

View File

@ -6,6 +6,7 @@ from grant.proposal.models import CATEGORIES
from grant.proposal.models import Proposal from grant.proposal.models import Proposal
from grant.user.models import User from grant.user.models import User
from ..config import BaseTestConfig from ..config import BaseTestConfig
from mock import patch
milestones = [ milestones = [
{ {
@ -182,7 +183,10 @@ class TestAPI(BaseTestConfig):
self.assertEqual(users_json["socialMedias"][0]["socialMediaLink"], team[0]["socialMedias"][0]["link"]) self.assertEqual(users_json["socialMedias"][0]["socialMediaLink"], team[0]["socialMedias"][0]["link"])
self.assertEqual(users_json["displayName"], team[0]["displayName"]) self.assertEqual(users_json["displayName"], team[0]["displayName"])
def test_create_user(self): @patch('grant.email.send.send_email')
def test_create_user(self, mock_send_email):
mock_send_email.return_value.ok = True
self.app.post( self.app.post(
"/api/v1/users/", "/api/v1/users/",
data=json.dumps(team[0]), data=json.dumps(team[0]),
@ -195,7 +199,9 @@ class TestAPI(BaseTestConfig):
self.assertEqual(user_db.title, team[0]["title"]) self.assertEqual(user_db.title, team[0]["title"])
self.assertEqual(user_db.account_address, team[0]["accountAddress"]) self.assertEqual(user_db.account_address, team[0]["accountAddress"])
def test_create_user_duplicate_400(self): @patch('grant.email.send.send_email')
def test_create_user_duplicate_400(self, mock_send_email):
mock_send_email.return_value.ok = True
self.test_create_user() self.test_create_user()
response = self.app.post( response = self.app.post(
@ -204,4 +210,4 @@ class TestAPI(BaseTestConfig):
content_type='application/json' content_type='application/json'
) )
self.assert400(response) self.assertEqual(response.status_code, 409)