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 grant import JSONResponse
from flask import Blueprint, jsonify
from animal_case import animalify
from .models import Comment, comments_schema
@ -11,4 +10,4 @@ blueprint = Blueprint("comment", __name__, url_prefix="/api/v1/comment")
def get_comments():
all_comments = Comment.query.all()
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
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():
milestones = Milestone.query.all()
result = milestones_schema.dump(milestones)
return JSONResponse(result)
return jsonify(animalify(result))

View File

@ -1,9 +1,10 @@
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 grant import JSONResponse
from grant.comment.models import Comment, comment_schema
from grant.milestone.models import Milestone
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()
if proposal:
dumped_proposal = proposal_schema.dump(proposal)
return JSONResponse(dumped_proposal)
return jsonify(animalify(dumped_proposal))
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"])
@ -27,22 +28,23 @@ def get_proposal_comments(proposal_id):
proposal = Proposal.query.filter_by(proposal_id=proposal_id).first()
if proposal:
dumped_proposal = proposal_schema.dump(proposal)
return JSONResponse(
return jsonify(animalify(
proposal_id=proposal_id,
total_comments=len(dumped_proposal["comments"]),
comments=dumped_proposal["comments"]
)
))
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"])
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()
if proposal:
incoming = request.get_json()
user_id = incoming["userId"]
content = incoming["content"]
user = User.query.filter_by(id=user_id).first()
if user:
@ -54,17 +56,19 @@ def post_proposal_comments(proposal_id):
db.session.add(comment)
db.session.commit()
dumped_comment = comment_schema.dump(comment)
return JSONResponse(dumped_comment, _statusCode=201)
return dumped_comment, 201
else:
return JSONResponse(message="No user matching id", _statusCode=404)
return {"message": "No user matching id"}, 404
else:
return JSONResponse(message="No proposal matching id", _statusCode=404)
return {"message": "No proposal matching id"}, 404
@blueprint.route("/", methods=["GET"])
def get_proposals():
stage = request.args.get("stage")
@endpoint.api(
parameter('stage', type=str, required=False)
)
def get_proposals(stage):
if stage:
proposals = (
Proposal.query.filter_by(stage=stage)
@ -74,24 +78,27 @@ def get_proposals():
else:
proposals = Proposal.query.order_by(Proposal.date_created.desc()).all()
dumped_proposals = proposals_schema.dump(proposals)
return JSONResponse(dumped_proposals)
return dumped_proposals
@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
incoming = request.get_json()
proposal_id = incoming["crowdFundContractAddress"]
content = incoming["content"]
title = incoming["title"]
milestones = incoming["milestones"]
category = incoming["category"]
existing_proposal = Proposal.query.filter_by(proposal_id=crowd_fund_contract_address).first()
if existing_proposal:
return {"message": "Oops! Something went wrong."}, 409
proposal = Proposal.create(
stage="FUNDING_REQUIRED",
proposal_id=proposal_id,
proposal_id=crowd_fund_contract_address,
content=content,
title=title,
category=category
@ -99,9 +106,8 @@ def make_proposal():
db.session.add(proposal)
team = incoming["team"]
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:
account_address = team_member.get("accountAddress")
@ -149,7 +155,7 @@ def make_proposal():
db.session.commit()
except IntegrityError as 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)
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 ..email.send import send_email
from ..proposal.models import Proposal, proposal_team
from ..utils.auth import requires_sm
from ..email.send import send_email
blueprint = Blueprint('user', __name__, url_prefix='/api/v1/users')
@blueprint.route("/", methods=["GET"])
def get_users():
proposal_query = request.args.get('proposalId')
proposal = Proposal.query.filter_by(proposal_id=proposal_query).first()
@endpoint.api(
parameter('proposalId', type=str, required=False)
)
def get_users(proposal_id):
proposal = Proposal.query.filter_by(proposal_id=proposal_id).first()
if not proposal:
users = User.query.all()
else:
users = User.query.join(proposal_team).join(Proposal) \
.filter(proposal_team.c.proposal_id == proposal.id).all()
result = users_schema.dump(users)
return JSONResponse(result)
return result
@blueprint.route("/me", methods=["GET"])
@requires_sm
def get_me():
dumped_user = user_schema.dump(g.current_user)
return JSONResponse(dumped_user)
return jsonify(animalify(dumped_user))
@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)
if user:
result = user_schema.dump(user)
return JSONResponse(result)
return jsonify(animalify(result))
else:
return JSONResponse(
message="User with account_address or user_identity matching {} not found".format(user_identity),
_statusCode=404)
return jsonify(
message="User with account_address or user_identity matching {} not found".format(user_identity)), 404
@blueprint.route("/", methods=["POST"])
def create_user():
incoming = request.get_json()
account_address = incoming["accountAddress"]
email_address = incoming["emailAddress"]
display_name = incoming["displayName"]
title = incoming["title"]
@endpoint.api(
parameter('accountAddress', type=str, required=True),
parameter('emailAddress', type=str, required=True),
parameter('displayName', type=str, required=True),
parameter('title', type=str, required=True),
)
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)
if existing_user:
return JSONResponse(
message="User with that address or email already exists",
_statusCode=400)
return {"message": "User with that address or email already exists"}, 409
# TODO: Handle avatar & social stuff too
user = User(
@ -73,4 +73,4 @@ def create_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
pep8-naming==0.7.0
pre-commit
flask_testing
flask_testing
mock

View File

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

View File

@ -48,7 +48,7 @@ class TestAPI(BaseTestConfig):
proposal_id=proposal["crowdFundContractAddress"]
).first())
self.app.post(
resp = self.app.post(
"/api/v1/proposals/",
data=json.dumps(proposal),
content_type='application/json'
@ -92,3 +92,18 @@ class TestAPI(BaseTestConfig):
)
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.user.models import User
from ..config import BaseTestConfig
from mock import patch
milestones = [
{
@ -182,7 +183,10 @@ class TestAPI(BaseTestConfig):
self.assertEqual(users_json["socialMedias"][0]["socialMediaLink"], team[0]["socialMedias"][0]["link"])
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(
"/api/v1/users/",
data=json.dumps(team[0]),
@ -195,7 +199,9 @@ class TestAPI(BaseTestConfig):
self.assertEqual(user_db.title, team[0]["title"])
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()
response = self.app.post(
@ -204,4 +210,4 @@ class TestAPI(BaseTestConfig):
content_type='application/json'
)
self.assert400(response)
self.assertEqual(response.status_code, 409)