Tighten Backend Patterns (#159)
This commit is contained in:
parent
eae0e81ff0
commit
357b4248c7
|
@ -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
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -53,3 +53,6 @@ markdownify
|
|||
# email
|
||||
flask-sendgrid==0.6
|
||||
sendgrid==5.3.0
|
||||
|
||||
# input validation
|
||||
flask-yolo2API
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue