Webargs Request Parsing (#228)

This commit is contained in:
Daniel Ternyak 2019-03-01 14:11:03 -06:00 committed by GitHub
parent f5d721c0b4
commit f950e17397
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 474 additions and 388 deletions

View File

@ -1,13 +1,18 @@
from functools import reduce
from flask import Blueprint, request, session
from flask_yoloapi import endpoint, parameter
from decimal import Decimal
from datetime import datetime from datetime import datetime
from sqlalchemy import text from decimal import Decimal
from functools import reduce
from flask import Blueprint, request
from marshmallow import fields, validate
from sqlalchemy import func, or_
import grant.utils.admin as admin
import grant.utils.auth as auth
from grant.comment.models import Comment, user_comments_schema, admin_comments_schema, admin_comment_schema from grant.comment.models import Comment, user_comments_schema, admin_comments_schema, admin_comment_schema
from grant.email.send import generate_email, send_email from grant.email.send import generate_email, send_email
from grant.extensions import db from grant.extensions import db
from grant.milestone.models import Milestone
from grant.parser import body, query, paginated_fields
from grant.proposal.models import ( from grant.proposal.models import (
Proposal, Proposal,
ProposalArbiter, ProposalArbiter,
@ -18,12 +23,11 @@ from grant.proposal.models import (
admin_proposal_contribution_schema, admin_proposal_contribution_schema,
admin_proposal_contributions_schema, admin_proposal_contributions_schema,
) )
from grant.milestone.models import Milestone
from grant.user.models import User, UserSettings, admin_users_schema, admin_user_schema
from grant.rfp.models import RFP, admin_rfp_schema, admin_rfps_schema from grant.rfp.models import RFP, admin_rfp_schema, admin_rfps_schema
import grant.utils.admin as admin from grant.settings import EXPLORER_URL
import grant.utils.auth as auth from grant.user.models import User, UserSettings, admin_users_schema, admin_user_schema
from grant.utils.misc import make_url from grant.utils import pagination
from grant.utils.enums import Category
from grant.utils.enums import ( from grant.utils.enums import (
ProposalStatus, ProposalStatus,
ProposalStage, ProposalStage,
@ -32,10 +36,7 @@ from grant.utils.enums import (
MilestoneStage, MilestoneStage,
RFPStatus, RFPStatus,
) )
from grant.utils import pagination from grant.utils.misc import make_url
from grant.settings import EXPLORER_URL
from sqlalchemy import func, or_
from .example_emails import example_email_args from .example_emails import example_email_args
blueprint = Blueprint('admin', __name__, url_prefix='/api/v1/admin') blueprint = Blueprint('admin', __name__, url_prefix='/api/v1/admin')
@ -59,16 +60,15 @@ def make_login_state():
@blueprint.route("/checklogin", methods=["GET"]) @blueprint.route("/checklogin", methods=["GET"])
@endpoint.api()
def loggedin(): def loggedin():
return make_login_state() return make_login_state()
@blueprint.route("/login", methods=["POST"]) @blueprint.route("/login", methods=["POST"])
@endpoint.api( @body({
parameter('username', type=str, required=False), "username": fields.Str(required=False, missing=None),
parameter('password', type=str, required=False), "password": fields.Str(required=False, missing=None)
) })
def login(username, password): def login(username, password):
if auth.auth_user(username, password): if auth.auth_user(username, password):
if admin.admin_is_authed(): if admin.admin_is_authed():
@ -77,9 +77,9 @@ def login(username, password):
@blueprint.route("/refresh", methods=["POST"]) @blueprint.route("/refresh", methods=["POST"])
@endpoint.api( @body({
parameter('password', type=str, required=True), "password": fields.Str(required=True)
) })
def refresh(password): def refresh(password):
if auth.refresh_auth(password): if auth.refresh_auth(password):
return make_login_state() return make_login_state()
@ -88,7 +88,6 @@ def refresh(password):
@blueprint.route("/2fa", methods=["GET"]) @blueprint.route("/2fa", methods=["GET"])
@endpoint.api()
def get_2fa(): def get_2fa():
if not admin.admin_is_authed(): if not admin.admin_is_authed():
return {"message": "Must be authenticated"}, 403 return {"message": "Must be authenticated"}, 403
@ -96,18 +95,17 @@ def get_2fa():
@blueprint.route("/2fa/init", methods=["GET"]) @blueprint.route("/2fa/init", methods=["GET"])
@endpoint.api()
def get_2fa_init(): def get_2fa_init():
admin.throw_on_2fa_not_allowed() admin.throw_on_2fa_not_allowed()
return admin.make_2fa_setup() return admin.make_2fa_setup()
@blueprint.route("/2fa/enable", methods=["POST"]) @blueprint.route("/2fa/enable", methods=["POST"])
@endpoint.api( @body({
parameter('backupCodes', type=list, required=True), "backupCodes": fields.List(fields.Str(), required=True),
parameter('totpSecret', type=str, required=True), "totpSecret": fields.Str(required=True),
parameter('verifyCode', type=str, required=True), "verifyCode": fields.Str(required=True)
) })
def post_2fa_enable(backup_codes, totp_secret, verify_code): def post_2fa_enable(backup_codes, totp_secret, verify_code):
admin.throw_on_2fa_not_allowed() admin.throw_on_2fa_not_allowed()
admin.check_and_set_2fa_setup(backup_codes, totp_secret, verify_code) admin.check_and_set_2fa_setup(backup_codes, totp_secret, verify_code)
@ -116,9 +114,9 @@ def post_2fa_enable(backup_codes, totp_secret, verify_code):
@blueprint.route("/2fa/verify", methods=["POST"]) @blueprint.route("/2fa/verify", methods=["POST"])
@endpoint.api( @body({
parameter('verifyCode', type=str, required=True), "verifyCode": fields.Str(required=True)
) })
def post_2fa_verify(verify_code): def post_2fa_verify(verify_code):
admin.throw_on_2fa_not_allowed(allow_stale=True) admin.throw_on_2fa_not_allowed(allow_stale=True)
admin.admin_auth_2fa(verify_code) admin.admin_auth_2fa(verify_code)
@ -127,7 +125,6 @@ def post_2fa_verify(verify_code):
@blueprint.route("/logout", methods=["GET"]) @blueprint.route("/logout", methods=["GET"])
@endpoint.api()
def logout(): def logout():
admin.logout() admin.logout()
return { return {
@ -137,7 +134,6 @@ def logout():
@blueprint.route("/stats", methods=["GET"]) @blueprint.route("/stats", methods=["GET"])
@endpoint.api()
@admin.admin_auth_required @admin.admin_auth_required
def stats(): def stats():
user_count = db.session.query(func.count(User.id)).scalar() user_count = db.session.query(func.count(User.id)).scalar()
@ -162,9 +158,9 @@ def stats():
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \ .filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
.join(Proposal) \ .join(Proposal) \
.filter(or_( .filter(or_(
Proposal.stage == ProposalStage.FAILED, Proposal.stage == ProposalStage.FAILED,
Proposal.stage == ProposalStage.CANCELED, Proposal.stage == ProposalStage.CANCELED,
)) \ )) \
.join(ProposalContribution.user) \ .join(ProposalContribution.user) \
.join(UserSettings) \ .join(UserSettings) \
.filter(UserSettings.refund_address != None) \ .filter(UserSettings.refund_address != None) \
@ -183,7 +179,6 @@ def stats():
@blueprint.route('/users/<user_id>', methods=['DELETE']) @blueprint.route('/users/<user_id>', methods=['DELETE'])
@endpoint.api()
@admin.admin_auth_required @admin.admin_auth_required
def delete_user(user_id): def delete_user(user_id):
user = User.query.filter(User.id == user_id).first() user = User.query.filter(User.id == user_id).first()
@ -192,16 +187,11 @@ def delete_user(user_id):
db.session.delete(user) db.session.delete(user)
db.session.commit() db.session.commit()
return None, 200 return {"message": "ok"}, 200
@blueprint.route("/users", methods=["GET"]) @blueprint.route("/users", methods=["GET"])
@endpoint.api( @query(paginated_fields)
parameter('page', type=int, required=False),
parameter('filters', type=list, required=False),
parameter('search', type=str, required=False),
parameter('sort', type=str, required=False)
)
@admin.admin_auth_required @admin.admin_auth_required
def get_users(page, filters, search, sort): def get_users(page, filters, search, sort):
filters_workaround = request.args.getlist('filters[]') filters_workaround = request.args.getlist('filters[]')
@ -217,7 +207,6 @@ def get_users(page, filters, search, sort):
@blueprint.route('/users/<id>', methods=['GET']) @blueprint.route('/users/<id>', methods=['GET'])
@endpoint.api()
@admin.admin_auth_required @admin.admin_auth_required
def get_user(id): def get_user(id):
user_db = User.query.filter(User.id == id).first() user_db = User.query.filter(User.id == id).first()
@ -235,12 +224,12 @@ def get_user(id):
@blueprint.route('/users/<user_id>', methods=['PUT']) @blueprint.route('/users/<user_id>', methods=['PUT'])
@endpoint.api( @body({
parameter('silenced', type=bool, required=False), "silenced": fields.Bool(required=False, missing=None),
parameter('banned', type=bool, required=False), "banned": fields.Bool(required=False, missing=None),
parameter('bannedReason', type=str, required=False), "bannedReason": fields.Str(required=False, missing=None),
parameter('isAdmin', type=bool, required=False) "isAdmin": fields.Bool(required=False, missing=None),
) })
@admin.admin_auth_required @admin.admin_auth_required
def edit_user(user_id, silenced, banned, banned_reason, is_admin): def edit_user(user_id, silenced, banned, banned_reason, is_admin):
user = User.query.filter(User.id == user_id).first() user = User.query.filter(User.id == user_id).first()
@ -266,9 +255,9 @@ def edit_user(user_id, silenced, banned, banned_reason, is_admin):
@blueprint.route("/arbiters", methods=["GET"]) @blueprint.route("/arbiters", methods=["GET"])
@endpoint.api( @query({
parameter('search', type=str, required=False), "search": fields.Str(required=False, missing=None)
) })
@admin.admin_auth_required @admin.admin_auth_required
def get_arbiters(search): def get_arbiters(search):
results = [] results = []
@ -289,10 +278,10 @@ def get_arbiters(search):
@blueprint.route('/arbiters', methods=['PUT']) @blueprint.route('/arbiters', methods=['PUT'])
@endpoint.api( @body({
parameter('proposalId', type=int, required=True), "proposalId": fields.Int(required=True),
parameter('userId', type=int, required=True) "userId": fields.Int(required=True),
) })
@admin.admin_auth_required @admin.admin_auth_required
def set_arbiter(proposal_id, user_id): def set_arbiter(proposal_id, user_id):
proposal = Proposal.query.filter(Proposal.id == proposal_id).first() proposal = Proposal.query.filter(Proposal.id == proposal_id).first()
@ -323,21 +312,16 @@ def set_arbiter(proposal_id, user_id):
db.session.commit() db.session.commit()
return { return {
'proposal': proposal_schema.dump(proposal), 'proposal': proposal_schema.dump(proposal),
'user': admin_user_schema.dump(user) 'user': admin_user_schema.dump(user)
}, 200 }, 200
# PROPOSALS # PROPOSALS
@blueprint.route("/proposals", methods=["GET"]) @blueprint.route("/proposals", methods=["GET"])
@endpoint.api( @query(paginated_fields)
parameter('page', type=int, required=False),
parameter('filters', type=list, required=False),
parameter('search', type=str, required=False),
parameter('sort', type=str, required=False)
)
@admin.admin_auth_required @admin.admin_auth_required
def get_proposals(page, filters, search, sort): def get_proposals(page, filters, search, sort):
filters_workaround = request.args.getlist('filters[]') filters_workaround = request.args.getlist('filters[]')
@ -353,7 +337,6 @@ def get_proposals(page, filters, search, sort):
@blueprint.route('/proposals/<id>', methods=['GET']) @blueprint.route('/proposals/<id>', methods=['GET'])
@endpoint.api()
@admin.admin_auth_required @admin.admin_auth_required
def get_proposal(id): def get_proposal(id):
proposal = Proposal.query.filter(Proposal.id == id).first() proposal = Proposal.query.filter(Proposal.id == id).first()
@ -363,16 +346,15 @@ def get_proposal(id):
@blueprint.route('/proposals/<id>', methods=['DELETE']) @blueprint.route('/proposals/<id>', methods=['DELETE'])
@endpoint.api()
@admin.admin_auth_required @admin.admin_auth_required
def delete_proposal(id): def delete_proposal(id):
return {"message": "Not implemented."}, 400 return {"message": "Not implemented."}, 400
@blueprint.route('/proposals/<id>', methods=['PUT']) @blueprint.route('/proposals/<id>', methods=['PUT'])
@endpoint.api( @body({
parameter('contributionMatching', type=float, required=False, default=None) "contributionMatching": fields.Int(required=False, missing=None)
) })
@admin.admin_auth_required @admin.admin_auth_required
def update_proposal(id, contribution_matching): def update_proposal(id, contribution_matching):
proposal = Proposal.query.filter(Proposal.id == id).first() proposal = Proposal.query.filter(Proposal.id == id).first()
@ -389,10 +371,10 @@ def update_proposal(id, contribution_matching):
@blueprint.route('/proposals/<id>/approve', methods=['PUT']) @blueprint.route('/proposals/<id>/approve', methods=['PUT'])
@endpoint.api( @body({
parameter('isApprove', type=bool, required=True), "isApprove": fields.Bool(required=True),
parameter('rejectReason', type=str, required=False) "rejectReason": fields.Str(required=False, missing=None)
) })
@admin.admin_auth_required @admin.admin_auth_required
def approve_proposal(id, is_approve, reject_reason=None): def approve_proposal(id, is_approve, reject_reason=None):
proposal = Proposal.query.filter_by(id=id).first() proposal = Proposal.query.filter_by(id=id).first()
@ -405,7 +387,6 @@ def approve_proposal(id, is_approve, reject_reason=None):
@blueprint.route('/proposals/<id>/cancel', methods=['PUT']) @blueprint.route('/proposals/<id>/cancel', methods=['PUT'])
@endpoint.api()
@admin.admin_auth_required @admin.admin_auth_required
def cancel_proposal(id): def cancel_proposal(id):
proposal = Proposal.query.filter_by(id=id).first() proposal = Proposal.query.filter_by(id=id).first()
@ -419,9 +400,9 @@ def cancel_proposal(id):
@blueprint.route("/proposals/<id>/milestone/<mid>/paid", methods=["PUT"]) @blueprint.route("/proposals/<id>/milestone/<mid>/paid", methods=["PUT"])
@endpoint.api( @body({
parameter('txId', type=str, required=True), "txId": fields.Str(required=True),
) })
@admin.admin_auth_required @admin.admin_auth_required
def paid_milestone_payout_request(id, mid, tx_id): def paid_milestone_payout_request(id, mid, tx_id):
proposal = Proposal.query.filter_by(id=id).first() proposal = Proposal.query.filter_by(id=id).first()
@ -459,7 +440,6 @@ def paid_milestone_payout_request(id, mid, tx_id):
@blueprint.route('/email/example/<type>', methods=['GET']) @blueprint.route('/email/example/<type>', methods=['GET'])
@endpoint.api()
@admin.admin_auth_required @admin.admin_auth_required
def get_email_example(type): def get_email_example(type):
email = generate_email(type, example_email_args.get(type)) email = generate_email(type, example_email_args.get(type))
@ -473,7 +453,6 @@ def get_email_example(type):
@blueprint.route('/rfps', methods=['GET']) @blueprint.route('/rfps', methods=['GET'])
@endpoint.api()
@admin.admin_auth_required @admin.admin_auth_required
def get_rfps(): def get_rfps():
rfps = RFP.query.all() rfps = RFP.query.all()
@ -481,15 +460,15 @@ def get_rfps():
@blueprint.route('/rfps', methods=['POST']) @blueprint.route('/rfps', methods=['POST'])
@endpoint.api( @body({
parameter('title', type=str), "title": fields.Str(required=True),
parameter('brief', type=str), "brief": fields.Str(required=True),
parameter('content', type=str), "content": fields.Str(required=True),
parameter('category', type=str), "category": fields.Str(required=True, validate=validate.OneOf(choices=Category.list())),
parameter('bounty', type=str), "bounty": fields.Str(required=False, missing=0),
parameter('matching', type=bool, default=False), "matching": fields.Bool(required=False, missing=False),
parameter('dateCloses', type=int), "dateCloses": fields.Int(required=True)
) })
@admin.admin_auth_required @admin.admin_auth_required
def create_rfp(date_closes, **kwargs): def create_rfp(date_closes, **kwargs):
rfp = RFP( rfp = RFP(
@ -498,11 +477,10 @@ def create_rfp(date_closes, **kwargs):
) )
db.session.add(rfp) db.session.add(rfp)
db.session.commit() db.session.commit()
return admin_rfp_schema.dump(rfp), 201 return admin_rfp_schema.dump(rfp), 200
@blueprint.route('/rfps/<rfp_id>', methods=['GET']) @blueprint.route('/rfps/<rfp_id>', methods=['GET'])
@endpoint.api()
@admin.admin_auth_required @admin.admin_auth_required
def get_rfp(rfp_id): def get_rfp(rfp_id):
rfp = RFP.query.filter(RFP.id == rfp_id).first() rfp = RFP.query.filter(RFP.id == rfp_id).first()
@ -513,16 +491,15 @@ def get_rfp(rfp_id):
@blueprint.route('/rfps/<rfp_id>', methods=['PUT']) @blueprint.route('/rfps/<rfp_id>', methods=['PUT'])
@endpoint.api( @body({
parameter('title', type=str), "title": fields.Str(required=True),
parameter('brief', type=str), "brief": fields.Str(required=True),
parameter('content', type=str), "content": fields.Str(required=True),
parameter('category', type=str), "category": fields.Str(required=True, validate=validate.OneOf(choices=Category.list())),
parameter('bounty', type=str), "bounty": fields.Str(required=True),
parameter('matching', type=bool, default=False), "matching": fields.Bool(required=True, default=False, missing=False),
parameter('dateCloses', type=int), "dateCloses": fields.Int(required=True)
parameter('status', type=str), })
)
@admin.admin_auth_required @admin.admin_auth_required
def update_rfp(rfp_id, title, brief, content, category, bounty, matching, date_closes, status): def update_rfp(rfp_id, title, brief, content, category, bounty, matching, date_closes, status):
rfp = RFP.query.filter(RFP.id == rfp_id).first() rfp = RFP.query.filter(RFP.id == rfp_id).first()
@ -552,7 +529,6 @@ def update_rfp(rfp_id, title, brief, content, category, bounty, matching, date_c
@blueprint.route('/rfps/<rfp_id>', methods=['DELETE']) @blueprint.route('/rfps/<rfp_id>', methods=['DELETE'])
@endpoint.api()
@admin.admin_auth_required @admin.admin_auth_required
def delete_rfp(rfp_id): def delete_rfp(rfp_id):
rfp = RFP.query.filter(RFP.id == rfp_id).first() rfp = RFP.query.filter(RFP.id == rfp_id).first()
@ -561,19 +537,14 @@ def delete_rfp(rfp_id):
db.session.delete(rfp) db.session.delete(rfp)
db.session.commit() db.session.commit()
return None, 200 return {"message": "ok"}, 200
# Contributions # Contributions
@blueprint.route('/contributions', methods=['GET']) @blueprint.route('/contributions', methods=['GET'])
@endpoint.api( @query(paginated_fields)
parameter('page', type=int, required=False),
parameter('filters', type=list, required=False),
parameter('search', type=str, required=False),
parameter('sort', type=str, required=False)
)
@admin.admin_auth_required @admin.admin_auth_required
def get_contributions(page, filters, search, sort): def get_contributions(page, filters, search, sort):
filters_workaround = request.args.getlist('filters[]') filters_workaround = request.args.getlist('filters[]')
@ -588,13 +559,14 @@ def get_contributions(page, filters, search, sort):
@blueprint.route('/contributions', methods=['POST']) @blueprint.route('/contributions', methods=['POST'])
@endpoint.api( @body({
parameter('proposalId', type=int, required=True), "proposalId": fields.Int(required=True),
parameter('userId', type=int, required=False, default=None), "userId": fields.Int(required=True),
parameter('status', type=str, required=True), # TODO guard status
parameter('amount', type=str, required=True), "status": fields.Str(required=True),
parameter('txId', type=str, required=False), "amount": fields.Str(required=True),
) "txId": fields.Str(required=False, missing=None)
})
@admin.admin_auth_required @admin.admin_auth_required
def create_contribution(proposal_id, user_id, status, amount, tx_id): def create_contribution(proposal_id, user_id, status, amount, tx_id):
# Some fields set manually since we're admin, and normally don't do this # Some fields set manually since we're admin, and normally don't do this
@ -603,6 +575,7 @@ def create_contribution(proposal_id, user_id, status, amount, tx_id):
user_id=user_id, user_id=user_id,
amount=amount, amount=amount,
) )
# TODO guard status
contribution.status = status contribution.status = status
contribution.tx_id = tx_id contribution.tx_id = tx_id
@ -617,7 +590,6 @@ def create_contribution(proposal_id, user_id, status, amount, tx_id):
@blueprint.route('/contributions/<contribution_id>', methods=['GET']) @blueprint.route('/contributions/<contribution_id>', methods=['GET'])
@endpoint.api()
@admin.admin_auth_required @admin.admin_auth_required
def get_contribution(contribution_id): def get_contribution(contribution_id):
contribution = ProposalContribution.query.filter(ProposalContribution.id == contribution_id).first() contribution = ProposalContribution.query.filter(ProposalContribution.id == contribution_id).first()
@ -628,14 +600,14 @@ def get_contribution(contribution_id):
@blueprint.route('/contributions/<contribution_id>', methods=['PUT']) @blueprint.route('/contributions/<contribution_id>', methods=['PUT'])
@endpoint.api( @body({
parameter('proposalId', type=int, required=False), "proposalId": fields.Int(required=False, missing=None),
parameter('userId', type=int, required=False), "userId": fields.Int(required=False, missing=None),
parameter('status', type=str, required=False), # TODO guard status
parameter('amount', type=str, required=False), "status": fields.Str(required=False, missing=None),
parameter('txId', type=str, required=False), "amount": fields.Str(required=False, missing=None),
parameter('refundTxId', type=str, required=False), "txId": fields.Str(required=False, missing=None)
) })
@admin.admin_auth_required @admin.admin_auth_required
def edit_contribution(contribution_id, proposal_id, user_id, status, amount, tx_id, refund_tx_id): def edit_contribution(contribution_id, proposal_id, user_id, status, amount, tx_id, refund_tx_id):
contribution = ProposalContribution.query.filter(ProposalContribution.id == contribution_id).first() contribution = ProposalContribution.query.filter(ProposalContribution.id == contribution_id).first()
@ -694,12 +666,12 @@ def edit_contribution(contribution_id, proposal_id, user_id, status, amount, tx_
@blueprint.route('/comments', methods=['GET']) @blueprint.route('/comments', methods=['GET'])
@endpoint.api( @body({
parameter('page', type=int, required=False), "page": fields.Int(required=False, missing=None),
parameter('filters', type=list, required=False), "filters": fields.List(fields.Str(), required=False, missing=None),
parameter('search', type=str, required=False), "search": fields.Str(required=False, missing=None),
parameter('sort', type=str, required=False) "sort": fields.Str(required=False, missing=None),
) })
@admin.admin_auth_required @admin.admin_auth_required
def get_comments(page, filters, search, sort): def get_comments(page, filters, search, sort):
filters_workaround = request.args.getlist('filters[]') filters_workaround = request.args.getlist('filters[]')
@ -714,10 +686,11 @@ def get_comments(page, filters, search, sort):
@blueprint.route('/comments/<comment_id>', methods=['PUT']) @blueprint.route('/comments/<comment_id>', methods=['PUT'])
@endpoint.api( @body({
parameter('hidden', type=bool, required=False), "hidden": fields.Bool(required=False, missing=None),
parameter('reported', type=bool, required=False), "reported": fields.Bool(required=False, missing=None),
)
})
@admin.admin_auth_required @admin.admin_auth_required
def edit_comment(comment_id, hidden, reported): def edit_comment(comment_id, hidden, reported):
comment = Comment.query.filter(Comment.id == comment_id).first() comment = Comment.query.filter(Comment.id == comment_id).first()

View File

@ -1,19 +1,54 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""The app module, containing the app factory function.""" """The app module, containing the app factory function."""
import sentry_sdk import sentry_sdk
from flask import Flask from animal_case import animalify
from flask import Flask, Response, jsonify
from flask_cors import CORS from flask_cors import CORS
from flask_security import SQLAlchemyUserDatastore from flask_security import SQLAlchemyUserDatastore
from flask_sslify import SSLify from flask_sslify import SSLify
from sentry_sdk.integrations.flask import FlaskIntegration
from grant import commands, proposal, user, comment, milestone, admin, email, blockchain, task, rfp from grant import commands, proposal, user, comment, milestone, admin, email, blockchain, task, rfp
from grant.extensions import bcrypt, migrate, db, ma, security from grant.extensions import bcrypt, migrate, db, ma, security
from grant.settings import SENTRY_RELEASE, ENV from grant.settings import SENTRY_RELEASE, ENV
from sentry_sdk.integrations.flask import FlaskIntegration
from grant.utils.auth import AuthException, handle_auth_error, get_authed_user from grant.utils.auth import AuthException, handle_auth_error, get_authed_user
from grant.utils.exceptions import ValidationException
class JSONResponse(Response):
@classmethod
def force_type(cls, rv, environ=None):
if isinstance(rv, dict) or isinstance(rv, list) or isinstance(rv, tuple):
rv = jsonify(animalify(rv))
elif rv is None:
rv = jsonify(data=None), 204
return super(JSONResponse, cls).force_type(rv, environ)
def create_app(config_objects=["grant.settings"]): def create_app(config_objects=["grant.settings"]):
app = Flask(__name__.split(".")[0]) app = Flask(__name__.split(".")[0])
app.response_class = JSONResponse
# Return validation errors
@app.errorhandler(ValidationException)
def handle_validation_error(err):
return jsonify({"message": str(err)}), 400
@app.errorhandler(422)
@app.errorhandler(400)
def handle_error(err):
headers = err.data.get("headers", None)
messages = err.data.get("messages", "Invalid request.")
error_message = "Something went wrong with your request. That's all we know"
if type(messages) == dict:
if 'json' in messages:
error_message = messages['json'][0]
if headers:
return jsonify({"message": error_message}), err.code, headers
else:
return jsonify({"message": error_message}), err.code
for conf in config_objects: for conf in config_objects:
app.config.from_object(conf) app.config.from_object(conf)
app.url_map.strict_slashes = False app.url_map.strict_slashes = False
@ -85,7 +120,6 @@ def register_commands(app):
app.cli.add_command(commands.lint) app.cli.add_command(commands.lint)
app.cli.add_command(commands.clean) app.cli.add_command(commands.clean)
app.cli.add_command(commands.urls) app.cli.add_command(commands.urls)
app.cli.add_command(proposal.commands.create_proposal) app.cli.add_command(proposal.commands.create_proposal)
app.cli.add_command(proposal.commands.create_proposals) app.cli.add_command(proposal.commands.create_proposals)
app.cli.add_command(user.commands.delete_user) app.cli.add_command(user.commands.delete_user)

View File

@ -1,5 +1,5 @@
from flask import Blueprint from flask import Blueprint
from flask_yoloapi import endpoint
from grant.blockchain.bootstrap import send_bootstrap_data from grant.blockchain.bootstrap import send_bootstrap_data
from grant.utils.auth import internal_webhook from grant.utils.auth import internal_webhook
@ -8,7 +8,6 @@ blueprint = Blueprint("blockchain", __name__, url_prefix="/api/v1/blockchain")
@blueprint.route("/bootstrap", methods=["GET"]) @blueprint.route("/bootstrap", methods=["GET"])
@internal_webhook @internal_webhook
@endpoint.api()
def get_bootstrap_info(): def get_bootstrap_info():
print('Bootstrap data requested from blockchain watcher microservice...') print('Bootstrap data requested from blockchain watcher microservice...')
send_bootstrap_data() send_bootstrap_data()

View File

@ -1,14 +1,4 @@
from flask import Blueprint from flask import Blueprint
from flask_yoloapi import endpoint
from .models import Comment, comments_schema
blueprint = Blueprint("comment", __name__, url_prefix="/api/v1/comment") blueprint = Blueprint("comment", __name__, url_prefix="/api/v1/comment")
# Unused
# @blueprint.route("/", methods=["GET"])
# @endpoint.api()
# def get_comments():
# all_comments = Comment.query.all()
# result = comments_schema.dump(all_comments)
# return result

View File

@ -1,14 +1,11 @@
from flask import Blueprint from flask import Blueprint
from flask_yoloapi import endpoint
from .models import EmailVerification, db from .models import EmailVerification, db
from grant.utils.enums import ProposalArbiterStatus
blueprint = Blueprint("email", __name__, url_prefix="/api/v1/email") blueprint = Blueprint("email", __name__, url_prefix="/api/v1/email")
@blueprint.route("/<code>/verify", methods=["POST"]) @blueprint.route("/<code>/verify", methods=["POST"])
@endpoint.api()
def verify_email(code): def verify_email(code):
ev = EmailVerification.query.filter_by(code=code).first() ev = EmailVerification.query.filter_by(code=code).first()
if ev: if ev:
@ -20,7 +17,6 @@ def verify_email(code):
@blueprint.route("/<code>/unsubscribe", methods=["POST"]) @blueprint.route("/<code>/unsubscribe", methods=["POST"])
@endpoint.api()
def unsubscribe_email(code): def unsubscribe_email(code):
ev = EmailVerification.query.filter_by(code=code).first() ev = EmailVerification.query.filter_by(code=code).first()
if ev: if ev:
@ -32,7 +28,6 @@ def unsubscribe_email(code):
@blueprint.route("/<code>/arbiter/<proposal_id>", methods=["POST"]) @blueprint.route("/<code>/arbiter/<proposal_id>", methods=["POST"])
@endpoint.api()
def accept_arbiter(code, proposal_id): def accept_arbiter(code, proposal_id):
ev = EmailVerification.query.filter_by(code=code).first() ev = EmailVerification.query.filter_by(code=code).first()
if ev: if ev:

View File

@ -1,14 +1,4 @@
from flask import Blueprint from flask import Blueprint
from flask_yoloapi import endpoint
from .models import Milestone, milestones_schema
blueprint = Blueprint('milestone', __name__, url_prefix='/api/v1/milestones') blueprint = Blueprint('milestone', __name__, url_prefix='/api/v1/milestones')
# Unused
# @blueprint.route("/", methods=["GET"])
# @endpoint.api()
# def get_milestones():
# milestones = Milestone.query.all()
# result = milestones_schema.dump(milestones)
# return result

91
backend/grant/parser.py Normal file
View File

@ -0,0 +1,91 @@
import functools
from animal_case import animalify
from webargs.core import dict2schema
from webargs.flaskparser import FlaskParser, abort
from marshmallow import fields
try:
from collections.abc import Mapping
except ImportError:
from collections import Mapping
class Parser(FlaskParser):
DEFAULT_VALIDATION_STATUS = 400
def use_kwargs(self, *args, **kwargs):
kwargs["as_kwargs"] = True
return self.use_args(*args, **kwargs)
def use_args(
self,
argmap,
req=None,
locations=None,
as_kwargs=False,
validate=None,
error_status_code=None,
error_headers=None,
):
locations = locations or self.locations
request_obj = req
# Optimization: If argmap is passed as a dictionary, we only need
# to generate a Schema once
if isinstance(argmap, Mapping):
argmap = dict2schema(argmap)()
def decorator(func):
req_ = request_obj
@functools.wraps(func)
def wrapper(*args, **kwargs):
req_obj = req_
if not req_obj:
req_obj = self.get_request_from_view_args(func, args, kwargs)
# NOTE: At this point, argmap may be a Schema, or a callable
parsed_args = self.parse(
argmap,
req=req_obj,
locations=locations,
validate=validate,
error_status_code=error_status_code,
error_headers=error_headers,
)
if as_kwargs:
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# ONLY CHANGE FROM ORIGINAL
kwargs.update(animalify(parsed_args, types='snake'))
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
return func(*args, **kwargs)
else:
# Add parsed_args after other positional arguments
new_args = args + (parsed_args,)
return func(*new_args, **kwargs)
wrapper.__wrapped__ = func
return wrapper
return decorator
def handle_invalid_json_error(self, error, req, *args, **kwargs):
print(error)
abort(400, exc=error, messages={"json": ["Invalid JSON body."]})
parser = Parser()
use_args = parser.use_args
use_kwargs = parser.use_kwargs
# default kwargs
query = functools.partial(use_kwargs, locations=("query",))
body = functools.partial(use_kwargs, locations=("json",))
paginated_fields = {
"page": fields.Int(required=False, missing=None),
"filters": fields.List(fields.Str(), required=False, missing=None),
"search": fields.Str(required=False, missing=None),
"sort": fields.Str(required=False, missing=None)
}

View File

@ -1,13 +1,19 @@
from dateutil.parser import parse from datetime import datetime
from decimal import Decimal from decimal import Decimal
from flask import Blueprint, g, request from flask import Blueprint, g, request
from flask_yoloapi import endpoint, parameter from marshmallow import fields, validate
from sqlalchemy import or_
from grant.comment.models import Comment, comment_schema, comments_schema from grant.comment.models import Comment, comment_schema, comments_schema
from grant.email.send import send_email from grant.email.send import send_email
from grant.milestone.models import Milestone from grant.milestone.models import Milestone
from grant.settings import EXPLORER_URL, PROPOSAL_STAKING_AMOUNT from grant.parser import body, query, paginated_fields
from grant.user.models import User
from grant.rfp.models import RFP from grant.rfp.models import RFP
from grant.settings import EXPLORER_URL, PROPOSAL_STAKING_AMOUNT
from grant.task.jobs import ProposalDeadline
from grant.user.models import User
from grant.utils import pagination
from grant.utils.auth import ( from grant.utils.auth import (
requires_auth, requires_auth,
requires_team_member_auth, requires_team_member_auth,
@ -16,14 +22,10 @@ from grant.utils.auth import (
get_authed_user, get_authed_user,
internal_webhook internal_webhook
) )
from grant.utils.enums import Category
from grant.utils.enums import ProposalStatus, ProposalStage, ContributionStatus
from grant.utils.exceptions import ValidationException from grant.utils.exceptions import ValidationException
from grant.utils.misc import is_email, make_url, from_zat from grant.utils.misc import is_email, make_url, from_zat
from grant.utils.enums import ProposalStatus, ProposalStage, ContributionStatus
from grant.utils import pagination
from grant.task.jobs import ProposalDeadline
from sqlalchemy import or_
from datetime import datetime
from .models import ( from .models import (
Proposal, Proposal,
proposals_schema, proposals_schema,
@ -43,7 +45,6 @@ blueprint = Blueprint("proposal", __name__, url_prefix="/api/v1/proposals")
@blueprint.route("/<proposal_id>", methods=["GET"]) @blueprint.route("/<proposal_id>", methods=["GET"])
@endpoint.api()
def get_proposal(proposal_id): def get_proposal(proposal_id):
proposal = Proposal.query.filter_by(id=proposal_id).first() proposal = Proposal.query.filter_by(id=proposal_id).first()
if proposal: if proposal:
@ -60,12 +61,7 @@ def get_proposal(proposal_id):
@blueprint.route("/<proposal_id>/comments", methods=["GET"]) @blueprint.route("/<proposal_id>/comments", methods=["GET"])
@endpoint.api( @query(paginated_fields)
parameter('page', type=int, required=False),
parameter('filters', type=list, required=False),
parameter('search', type=str, required=False),
parameter('sort', type=str, required=False)
)
def get_proposal_comments(proposal_id, page, filters, search, sort): def get_proposal_comments(proposal_id, page, filters, search, sort):
# only using page, currently # only using page, currently
filters_workaround = request.args.getlist('filters[]') filters_workaround = request.args.getlist('filters[]')
@ -82,7 +78,6 @@ def get_proposal_comments(proposal_id, page, filters, search, sort):
@blueprint.route("/<proposal_id>/comments/<comment_id>/report", methods=["PUT"]) @blueprint.route("/<proposal_id>/comments/<comment_id>/report", methods=["PUT"])
@requires_email_verified_auth @requires_email_verified_auth
@endpoint.api()
def report_proposal_comment(proposal_id, comment_id): def report_proposal_comment(proposal_id, comment_id):
# Make sure proposal exists # Make sure proposal exists
proposal = Proposal.query.filter_by(id=proposal_id).first() proposal = Proposal.query.filter_by(id=proposal_id).first()
@ -95,15 +90,15 @@ def report_proposal_comment(proposal_id, comment_id):
comment.report(True) comment.report(True)
db.session.commit() db.session.commit()
return None, 200 return {"message": "ok"}, 200
@blueprint.route("/<proposal_id>/comments", methods=["POST"]) @blueprint.route("/<proposal_id>/comments", methods=["POST"])
@requires_email_verified_auth @requires_email_verified_auth
@endpoint.api( @body({
parameter('comment', type=str, required=True), "comment": fields.Str(required=True),
parameter('parentCommentId', type=int, required=False) "parentCommentId": fields.Int(required=False, missing=None),
) })
def post_proposal_comments(proposal_id, comment, parent_comment_id): def post_proposal_comments(proposal_id, comment, parent_comment_id):
# Make sure proposal exists # Make sure proposal exists
proposal = Proposal.query.filter_by(id=proposal_id).first() proposal = Proposal.query.filter_by(id=proposal_id).first()
@ -161,12 +156,7 @@ def post_proposal_comments(proposal_id, comment, parent_comment_id):
@blueprint.route("/", methods=["GET"]) @blueprint.route("/", methods=["GET"])
@endpoint.api( @query(paginated_fields)
parameter('page', type=int, required=False),
parameter('filters', type=list, required=False),
parameter('search', type=str, required=False),
parameter('sort', type=str, required=False)
)
def get_proposals(page, filters, search, sort): def get_proposals(page, filters, search, sort):
filters_workaround = request.args.getlist('filters[]') filters_workaround = request.args.getlist('filters[]')
query = Proposal.query.filter_by(status=ProposalStatus.LIVE) \ query = Proposal.query.filter_by(status=ProposalStatus.LIVE) \
@ -185,9 +175,9 @@ def get_proposals(page, filters, search, sort):
@blueprint.route("/drafts", methods=["POST"]) @blueprint.route("/drafts", methods=["POST"])
@requires_email_verified_auth @requires_email_verified_auth
@endpoint.api( @body({
parameter('rfpId', type=int), "rfpId": fields.Int(required=False, missing=None)
) })
def make_proposal_draft(rfp_id): def make_proposal_draft(rfp_id):
proposal = Proposal.create(status=ProposalStatus.DRAFT) proposal = Proposal.create(status=ProposalStatus.DRAFT)
proposal.team.append(g.current_user) proposal.team.append(g.current_user)
@ -209,34 +199,34 @@ def make_proposal_draft(rfp_id):
@blueprint.route("/drafts", methods=["GET"]) @blueprint.route("/drafts", methods=["GET"])
@requires_auth @requires_auth
@endpoint.api()
def get_proposal_drafts(): def get_proposal_drafts():
proposals = ( proposals = (
Proposal.query Proposal.query
.filter(or_( .filter(or_(
Proposal.status == ProposalStatus.DRAFT, Proposal.status == ProposalStatus.DRAFT,
Proposal.status == ProposalStatus.REJECTED, Proposal.status == ProposalStatus.REJECTED,
)) ))
.join(proposal_team) .join(proposal_team)
.filter(proposal_team.c.user_id == g.current_user.id) .filter(proposal_team.c.user_id == g.current_user.id)
.order_by(Proposal.date_created.desc()) .order_by(Proposal.date_created.desc())
.all() .all()
) )
return proposals_schema.dump(proposals), 200 return proposals_schema.dump(proposals), 200
@blueprint.route("/<proposal_id>", methods=["PUT"]) @blueprint.route("/<proposal_id>", methods=["PUT"])
@requires_team_member_auth @requires_team_member_auth
@endpoint.api( # TODO add gaurd (minimum, maximum, shape)
parameter('title', type=str), @body({
parameter('brief', type=str), "title": fields.Str(required=True),
parameter('category', type=str), "brief": fields.Str(required=True),
parameter('content', type=str), "category": fields.Str(required=True, validate=validate.OneOf(choices=Category.list())),
parameter('target', type=str), "content": fields.Str(required=True),
parameter('payoutAddress', type=str), "target": fields.Str(required=True),
parameter('deadlineDuration', type=int), "payoutAddress": fields.Str(required=True),
parameter('milestones', type=list) "deadlineDuration": fields.Int(required=True),
) "milestones": fields.List(fields.Dict(), required=True),
})
def update_proposal(milestones, proposal_id, **kwargs): def update_proposal(milestones, proposal_id, **kwargs):
# Update the base proposal fields # Update the base proposal fields
try: try:
@ -251,9 +241,9 @@ def update_proposal(milestones, proposal_id, **kwargs):
m = Milestone( m = Milestone(
title=mdata["title"], title=mdata["title"],
content=mdata["content"], content=mdata["content"],
date_estimated=datetime.fromtimestamp(mdata["dateEstimated"]), date_estimated=datetime.fromtimestamp(mdata["date_estimated"]),
payout_percent=str(mdata["payoutPercent"]), payout_percent=str(mdata["payout_percent"]),
immediate_payout=mdata["immediatePayout"], immediate_payout=mdata["immediate_payout"],
proposal_id=g.current_proposal.id, proposal_id=g.current_proposal.id,
index=i index=i
) )
@ -266,7 +256,6 @@ def update_proposal(milestones, proposal_id, **kwargs):
@blueprint.route("/<proposal_id>/rfp", methods=["DELETE"]) @blueprint.route("/<proposal_id>/rfp", methods=["DELETE"])
@requires_team_member_auth @requires_team_member_auth
@endpoint.api()
def unlink_proposal_from_rfp(proposal_id): def unlink_proposal_from_rfp(proposal_id):
g.current_proposal.rfp_id = None g.current_proposal.rfp_id = None
db.session.add(g.current_proposal) db.session.add(g.current_proposal)
@ -276,7 +265,6 @@ def unlink_proposal_from_rfp(proposal_id):
@blueprint.route("/<proposal_id>", methods=["DELETE"]) @blueprint.route("/<proposal_id>", methods=["DELETE"])
@requires_team_member_auth @requires_team_member_auth
@endpoint.api()
def delete_proposal(proposal_id): def delete_proposal(proposal_id):
deleteable_statuses = [ deleteable_statuses = [
ProposalStatus.DRAFT, ProposalStatus.DRAFT,
@ -290,12 +278,11 @@ def delete_proposal(proposal_id):
return {"message": "Cannot delete proposals with %s status" % status}, 400 return {"message": "Cannot delete proposals with %s status" % status}, 400
db.session.delete(g.current_proposal) db.session.delete(g.current_proposal)
db.session.commit() db.session.commit()
return None, 202 return {"message": "ok"}, 202
@blueprint.route("/<proposal_id>/submit_for_approval", methods=["PUT"]) @blueprint.route("/<proposal_id>/submit_for_approval", methods=["PUT"])
@requires_team_member_auth @requires_team_member_auth
@endpoint.api()
def submit_for_approval_proposal(proposal_id): def submit_for_approval_proposal(proposal_id):
try: try:
g.current_proposal.submit_for_approval() g.current_proposal.submit_for_approval()
@ -308,19 +295,17 @@ def submit_for_approval_proposal(proposal_id):
@blueprint.route("/<proposal_id>/stake", methods=["GET"]) @blueprint.route("/<proposal_id>/stake", methods=["GET"])
@requires_team_member_auth @requires_team_member_auth
@endpoint.api()
def get_proposal_stake(proposal_id): def get_proposal_stake(proposal_id):
if g.current_proposal.status != ProposalStatus.STAKING: if g.current_proposal.status != ProposalStatus.STAKING:
return None, 400 return {"message": "ok"}, 400
contribution = g.current_proposal.get_staking_contribution(g.current_user.id) contribution = g.current_proposal.get_staking_contribution(g.current_user.id)
if contribution: if contribution:
return proposal_contribution_schema.dump(contribution) return proposal_contribution_schema.dump(contribution)
return None, 404 return {"message": "ok"}, 404
@blueprint.route("/<proposal_id>/publish", methods=["PUT"]) @blueprint.route("/<proposal_id>/publish", methods=["PUT"])
@requires_team_member_auth @requires_team_member_auth
@endpoint.api()
def publish_proposal(proposal_id): def publish_proposal(proposal_id):
try: try:
g.current_proposal.publish() g.current_proposal.publish()
@ -336,7 +321,6 @@ def publish_proposal(proposal_id):
@blueprint.route("/<proposal_id>/updates", methods=["GET"]) @blueprint.route("/<proposal_id>/updates", methods=["GET"])
@endpoint.api()
def get_proposal_updates(proposal_id): def get_proposal_updates(proposal_id):
proposal = Proposal.query.filter_by(id=proposal_id).first() proposal = Proposal.query.filter_by(id=proposal_id).first()
if proposal: if proposal:
@ -347,7 +331,6 @@ def get_proposal_updates(proposal_id):
@blueprint.route("/<proposal_id>/updates/<update_id>", methods=["GET"]) @blueprint.route("/<proposal_id>/updates/<update_id>", methods=["GET"])
@endpoint.api()
def get_proposal_update(proposal_id, update_id): def get_proposal_update(proposal_id, update_id):
proposal = Proposal.query.filter_by(id=proposal_id).first() proposal = Proposal.query.filter_by(id=proposal_id).first()
if proposal: if proposal:
@ -362,10 +345,10 @@ def get_proposal_update(proposal_id, update_id):
@blueprint.route("/<proposal_id>/updates", methods=["POST"]) @blueprint.route("/<proposal_id>/updates", methods=["POST"])
@requires_team_member_auth @requires_team_member_auth
@endpoint.api( @body({
parameter('title', type=str, required=True), "title": fields.Str(required=True),
parameter('content', type=str, required=True) "content": fields.Str(required=True)
) })
def post_proposal_update(proposal_id, title, content): def post_proposal_update(proposal_id, title, content):
update = ProposalUpdate( update = ProposalUpdate(
proposal_id=g.current_proposal.id, proposal_id=g.current_proposal.id,
@ -391,9 +374,9 @@ def post_proposal_update(proposal_id, title, content):
@blueprint.route("/<proposal_id>/invite", methods=["POST"]) @blueprint.route("/<proposal_id>/invite", methods=["POST"])
@requires_team_member_auth @requires_team_member_auth
@endpoint.api( @body({
parameter('address', type=str, required=True) "address": fields.Str(required=True),
) })
def post_proposal_team_invite(proposal_id, address): def post_proposal_team_invite(proposal_id, address):
invite = ProposalTeamInvite( invite = ProposalTeamInvite(
proposal_id=proposal_id, proposal_id=proposal_id,
@ -422,7 +405,6 @@ def post_proposal_team_invite(proposal_id, address):
@blueprint.route("/<proposal_id>/invite/<id_or_address>", methods=["DELETE"]) @blueprint.route("/<proposal_id>/invite/<id_or_address>", methods=["DELETE"])
@requires_team_member_auth @requires_team_member_auth
@endpoint.api()
def delete_proposal_team_invite(proposal_id, id_or_address): def delete_proposal_team_invite(proposal_id, id_or_address):
invite = ProposalTeamInvite.query.filter( invite = ProposalTeamInvite.query.filter(
(ProposalTeamInvite.id == id_or_address) | (ProposalTeamInvite.id == id_or_address) |
@ -435,11 +417,10 @@ def delete_proposal_team_invite(proposal_id, id_or_address):
db.session.delete(invite) db.session.delete(invite)
db.session.commit() db.session.commit()
return None, 202 return {"message": "ok"}, 202
@blueprint.route("/<proposal_id>/contributions", methods=["GET"]) @blueprint.route("/<proposal_id>/contributions", methods=["GET"])
@endpoint.api()
def get_proposal_contributions(proposal_id): def get_proposal_contributions(proposal_id):
proposal = Proposal.query.filter_by(id=proposal_id).first() proposal = Proposal.query.filter_by(id=proposal_id).first()
if not proposal: if not proposal:
@ -447,19 +428,19 @@ def get_proposal_contributions(proposal_id):
top_contributions = ProposalContribution.query \ top_contributions = ProposalContribution.query \
.filter_by( .filter_by(
proposal_id=proposal_id, proposal_id=proposal_id,
status=ContributionStatus.CONFIRMED, status=ContributionStatus.CONFIRMED,
staking=False, staking=False,
) \ ) \
.order_by(ProposalContribution.amount.desc()) \ .order_by(ProposalContribution.amount.desc()) \
.limit(5) \ .limit(5) \
.all() .all()
latest_contributions = ProposalContribution.query \ latest_contributions = ProposalContribution.query \
.filter_by( .filter_by(
proposal_id=proposal_id, proposal_id=proposal_id,
status=ContributionStatus.CONFIRMED, status=ContributionStatus.CONFIRMED,
staking=False, staking=False,
) \ ) \
.order_by(ProposalContribution.date_created.desc()) \ .order_by(ProposalContribution.date_created.desc()) \
.limit(5) \ .limit(5) \
.all() .all()
@ -471,7 +452,6 @@ def get_proposal_contributions(proposal_id):
@blueprint.route("/<proposal_id>/contributions/<contribution_id>", methods=["GET"]) @blueprint.route("/<proposal_id>/contributions/<contribution_id>", methods=["GET"])
@endpoint.api()
def get_proposal_contribution(proposal_id, contribution_id): def get_proposal_contribution(proposal_id, contribution_id):
proposal = Proposal.query.filter_by(id=proposal_id).first() proposal = Proposal.query.filter_by(id=proposal_id).first()
if proposal: if proposal:
@ -485,10 +465,11 @@ def get_proposal_contribution(proposal_id, contribution_id):
@blueprint.route("/<proposal_id>/contributions", methods=["POST"]) @blueprint.route("/<proposal_id>/contributions", methods=["POST"])
@endpoint.api( # TODO add gaurd (minimum, maximum)
parameter('amount', type=str, required=True), @body({
parameter('anonymous', type=bool, required=False) "amount": fields.Str(required=True),
) "anonymous": fields.Bool(required=False, missing=None)
})
def post_proposal_contribution(proposal_id, amount, anonymous): def post_proposal_contribution(proposal_id, amount, anonymous):
proposal = Proposal.query.filter_by(id=proposal_id).first() proposal = Proposal.query.filter_by(id=proposal_id).first()
if not proposal: if not proposal:
@ -518,11 +499,11 @@ def post_proposal_contribution(proposal_id, amount, anonymous):
# Can't use <proposal_id> since webhook doesn't know proposal id # Can't use <proposal_id> since webhook doesn't know proposal id
@blueprint.route("/contribution/<contribution_id>/confirm", methods=["POST"]) @blueprint.route("/contribution/<contribution_id>/confirm", methods=["POST"])
@internal_webhook @internal_webhook
@endpoint.api( @body({
parameter('to', type=str, required=True), "to": fields.Str(required=True),
parameter('amount', type=str, required=True), "amount": fields.Str(required=True),
parameter('txid', type=str, required=True), "txid": fields.Str(required=True),
) })
def post_contribution_confirmation(contribution_id, to, amount, txid): def post_contribution_confirmation(contribution_id, to, amount, txid):
contribution = ProposalContribution.query.filter_by( contribution = ProposalContribution.query.filter_by(
id=contribution_id).first() id=contribution_id).first()
@ -534,7 +515,7 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
if contribution.status == ContributionStatus.CONFIRMED: if contribution.status == ContributionStatus.CONFIRMED:
# Duplicates can happen, just return ok # Duplicates can happen, just return ok
return None, 200 return {"message": "ok"}, 200
# Convert to whole zcash coins from zats # Convert to whole zcash coins from zats
zec_amount = str(from_zat(int(amount))) zec_amount = str(from_zat(int(amount)))
@ -581,14 +562,13 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
contribution.proposal.set_funded_when_ready() contribution.proposal.set_funded_when_ready()
db.session.commit() db.session.commit()
return None, 200 return {"message": "ok"}, 200
@blueprint.route("/contribution/<contribution_id>", methods=["DELETE"]) @blueprint.route("/contribution/<contribution_id>", methods=["DELETE"])
@requires_auth @requires_auth
@endpoint.api()
def delete_proposal_contribution(contribution_id): def delete_proposal_contribution(contribution_id):
contribution = contribution = ProposalContribution.query.filter_by( contribution = ProposalContribution.query.filter_by(
id=contribution_id).first() id=contribution_id).first()
if not contribution: if not contribution:
return {"message": "No contribution matching id"}, 404 return {"message": "No contribution matching id"}, 404
@ -602,13 +582,12 @@ def delete_proposal_contribution(contribution_id):
contribution.status = ContributionStatus.DELETED contribution.status = ContributionStatus.DELETED
db.session.add(contribution) db.session.add(contribution)
db.session.commit() db.session.commit()
return None, 202 return {"message": "ok"}, 202
# request MS payout # request MS payout
@blueprint.route("/<proposal_id>/milestone/<milestone_id>/request", methods=["PUT"]) @blueprint.route("/<proposal_id>/milestone/<milestone_id>/request", methods=["PUT"])
@requires_team_member_auth @requires_team_member_auth
@endpoint.api()
def request_milestone_payout(proposal_id, milestone_id): def request_milestone_payout(proposal_id, milestone_id):
if not g.current_proposal.is_funded: if not g.current_proposal.is_funded:
return {"message": "Proposal is not fully funded"}, 400 return {"message": "Proposal is not fully funded"}, 400
@ -630,7 +609,6 @@ def request_milestone_payout(proposal_id, milestone_id):
# accept MS payout (arbiter) # accept MS payout (arbiter)
@blueprint.route("/<proposal_id>/milestone/<milestone_id>/accept", methods=["PUT"]) @blueprint.route("/<proposal_id>/milestone/<milestone_id>/accept", methods=["PUT"])
@requires_arbiter_auth @requires_arbiter_auth
@endpoint.api()
def accept_milestone_payout_request(proposal_id, milestone_id): def accept_milestone_payout_request(proposal_id, milestone_id):
if not g.current_proposal.is_funded: if not g.current_proposal.is_funded:
return {"message": "Proposal is not fully funded"}, 400 return {"message": "Proposal is not fully funded"}, 400
@ -655,9 +633,9 @@ def accept_milestone_payout_request(proposal_id, milestone_id):
# reject MS payout (arbiter) (reason) # reject MS payout (arbiter) (reason)
@blueprint.route("/<proposal_id>/milestone/<milestone_id>/reject", methods=["PUT"]) @blueprint.route("/<proposal_id>/milestone/<milestone_id>/reject", methods=["PUT"])
@requires_arbiter_auth @requires_arbiter_auth
@endpoint.api( @body({
parameter('reason', type=str, required=True), "reason": fields.Str(required=True)
) })
def reject_milestone_payout_request(proposal_id, milestone_id, reason): def reject_milestone_payout_request(proposal_id, milestone_id, reason):
if not g.current_proposal.is_funded: if not g.current_proposal.is_funded:
return {"message": "Proposal is not fully funded"}, 400 return {"message": "Proposal is not fully funded"}, 400

View File

@ -2,6 +2,7 @@ from datetime import datetime
from grant.extensions import ma, db from grant.extensions import ma, db
from grant.utils.enums import RFPStatus from grant.utils.enums import RFPStatus
from grant.utils.misc import dt_to_unix from grant.utils.misc import dt_to_unix
from grant.utils.enums import Category
class RFP(db.Model): class RFP(db.Model):
@ -46,6 +47,8 @@ class RFP(db.Model):
matching: bool = False, matching: bool = False,
status: str = RFPStatus.DRAFT, status: str = RFPStatus.DRAFT,
): ):
# TODO add status assert
assert Category.includes(category)
self.date_created = datetime.now() self.date_created = datetime.now()
self.title = title self.title = title
self.brief = brief self.brief = brief

View File

@ -1,5 +1,4 @@
from flask import Blueprint, g from flask import Blueprint
from flask_yoloapi import endpoint, parameter
from sqlalchemy import or_ from sqlalchemy import or_
from grant.utils.enums import RFPStatus from grant.utils.enums import RFPStatus
@ -9,20 +8,18 @@ blueprint = Blueprint("rfp", __name__, url_prefix="/api/v1/rfps")
@blueprint.route("/", methods=["GET"]) @blueprint.route("/", methods=["GET"])
@endpoint.api()
def get_rfps(): def get_rfps():
rfps = RFP.query \ rfps = RFP.query \
.filter(or_( .filter(or_(
RFP.status == RFPStatus.LIVE, RFP.status == RFPStatus.LIVE,
RFP.status == RFPStatus.CLOSED, RFP.status == RFPStatus.CLOSED,
)) \ )) \
.order_by(RFP.date_created.desc()) \ .order_by(RFP.date_created.desc()) \
.all() .all()
return rfps_schema.dump(rfps) return rfps_schema.dump(rfps)
@blueprint.route("/<rfp_id>", methods=["GET"]) @blueprint.route("/<rfp_id>", methods=["GET"])
@endpoint.api()
def get_rfp(rfp_id): def get_rfp(rfp_id):
rfp = RFP.query.filter_by(id=rfp_id).first() rfp = RFP.query.filter_by(id=rfp_id).first()
if not rfp or rfp.status == RFPStatus.DRAFT: if not rfp or rfp.status == RFPStatus.DRAFT:

View File

@ -1,8 +1,11 @@
from animal_case import keys_to_snake_case from animal_case import keys_to_snake_case
from flask import Blueprint, g from flask import Blueprint, g
from flask_yoloapi import endpoint, parameter from marshmallow import fields
import grant.utils.auth as auth
from grant.comment.models import Comment, user_comments_schema from grant.comment.models import Comment, user_comments_schema
from grant.email.models import EmailRecovery from grant.email.models import EmailRecovery
from grant.parser import query, body
from grant.proposal.models import ( from grant.proposal.models import (
Proposal, Proposal,
proposal_team, proposal_team,
@ -13,12 +16,10 @@ from grant.proposal.models import (
user_proposals_schema, user_proposals_schema,
user_proposal_arbiters_schema user_proposal_arbiters_schema
) )
import grant.utils.auth as auth from grant.utils.enums import ProposalStatus, ContributionStatus
from grant.utils.exceptions import ValidationException from grant.utils.exceptions import ValidationException
from grant.utils.social import verify_social, get_social_login_url, VerifySocialException from grant.utils.social import verify_social, get_social_login_url, VerifySocialException
from grant.utils.upload import remove_avatar, sign_avatar_upload, AvatarException from grant.utils.upload import remove_avatar, sign_avatar_upload, AvatarException
from grant.utils.enums import ProposalStatus, ContributionStatus
from flask import current_app
from .models import ( from .models import (
User, User,
SocialMedia, SocialMedia,
@ -34,9 +35,9 @@ blueprint = Blueprint('user', __name__, url_prefix='/api/v1/users')
@blueprint.route("/", methods=["GET"]) @blueprint.route("/", methods=["GET"])
@endpoint.api( @query({
parameter('proposalId', type=str, required=False) "proposalId": fields.Str(required=False, missing=None)
) })
def get_users(proposal_id): def get_users(proposal_id):
proposal = Proposal.query.filter_by(id=proposal_id).first() proposal = Proposal.query.filter_by(id=proposal_id).first()
if not proposal: if not proposal:
@ -44,10 +45,10 @@ def get_users(proposal_id):
else: else:
users = ( users = (
User.query User.query
.join(proposal_team) .join(proposal_team)
.join(Proposal) .join(Proposal)
.filter(proposal_team.c.proposal_id == proposal.id) .filter(proposal_team.c.proposal_id == proposal.id)
.all() .all()
) )
result = users_schema.dump(users) result = users_schema.dump(users)
return result return result
@ -55,20 +56,19 @@ def get_users(proposal_id):
@blueprint.route("/me", methods=["GET"]) @blueprint.route("/me", methods=["GET"])
@auth.requires_auth @auth.requires_auth
@endpoint.api()
def get_me(): def get_me():
dumped_user = self_user_schema.dump(g.current_user) dumped_user = self_user_schema.dump(g.current_user)
return dumped_user return dumped_user
@blueprint.route("/<user_id>", methods=["GET"]) @blueprint.route("/<user_id>", methods=["GET"])
@endpoint.api( @query({
parameter("withProposals", type=bool, required=False), "withProposals": fields.Bool(required=False, missing=None),
parameter("withComments", type=bool, required=False), "withComments": fields.Bool(required=False, missing=None),
parameter("withFunded", type=bool, required=False), "withFunded": fields.Bool(required=False, missing=None),
parameter("withPending", type=bool, required=False), "withPending": fields.Bool(required=False, missing=None),
parameter("withArbitrated", type=bool, required=False) "withArbitrated": fields.Bool(required=False, missing=None)
) })
def get_user(user_id, with_proposals, with_comments, with_funded, with_pending, with_arbitrated): def get_user(user_id, with_proposals, with_comments, with_funded, with_pending, with_arbitrated):
user = User.get_by_id(user_id) user = User.get_by_id(user_id)
if user: if user:
@ -109,12 +109,13 @@ def get_user(user_id, with_proposals, with_comments, with_funded, with_pending,
@blueprint.route("/", methods=["POST"]) @blueprint.route("/", methods=["POST"])
@endpoint.api( @body({
parameter('emailAddress', type=str, required=True), # TODO guard all (valid, minimum, maximum)
parameter('password', type=str, required=True), "emailAddress": fields.Str(required=True),
parameter('displayName', type=str, required=True), "password": fields.Str(required=True),
parameter('title', type=str, required=True) "displayName": fields.Str(required=True),
) "title": fields.Str(required=True),
})
def create_user( def create_user(
email_address, email_address,
password, password,
@ -137,10 +138,10 @@ def create_user(
@blueprint.route("/auth", methods=["POST"]) @blueprint.route("/auth", methods=["POST"])
@endpoint.api( @body({
parameter('email', type=str, required=True), "email": fields.Str(required=True),
parameter('password', type=str, required=True) "password": fields.Str(required=True)
) })
def auth_user(email, password): def auth_user(email, password):
authed_user = auth.auth_user(email, password) authed_user = auth.auth_user(email, password)
return self_user_schema.dump(authed_user) return self_user_schema.dump(authed_user)
@ -148,49 +149,48 @@ def auth_user(email, password):
@blueprint.route("/me/password", methods=["PUT"]) @blueprint.route("/me/password", methods=["PUT"])
@auth.requires_auth @auth.requires_auth
@endpoint.api( # TODO gaurd password (minimum)
parameter('currentPassword', type=str, required=True), @body({
parameter('password', type=str, required=True), "currentPassword": fields.Str(required=True),
) "password": fields.Str(required=True)
})
def update_user_password(current_password, password): def update_user_password(current_password, password):
if not g.current_user.check_password(current_password): if not g.current_user.check_password(current_password):
return {"message": "Current password incorrect"}, 403 return {"message": "Current password incorrect"}, 403
g.current_user.set_password(password) g.current_user.set_password(password)
return None, 200 return {"message": "ok"}, 200
@blueprint.route("/me/email", methods=["PUT"]) @blueprint.route("/me/email", methods=["PUT"])
@auth.requires_auth @auth.requires_auth
@endpoint.api( # TODO gaurd all (valid, minimum)
parameter('email', type=str, required=True), @body({
parameter('password', type=str, required=True) "email": fields.Str(required=True),
) "password": fields.Str(required=True)
})
def update_user_email(email, password): def update_user_email(email, password):
if not g.current_user.check_password(password): if not g.current_user.check_password(password):
return {"message": "Password is incorrect"}, 403 return {"message": "Password is incorrect"}, 403
g.current_user.set_email(email) g.current_user.set_email(email)
return None, 200 return {"message": "ok"}, 200
@blueprint.route("/me/resend-verification", methods=["PUT"]) @blueprint.route("/me/resend-verification", methods=["PUT"])
@auth.requires_auth @auth.requires_auth
@endpoint.api()
def resend_email_verification(): def resend_email_verification():
g.current_user.send_verification_email() g.current_user.send_verification_email()
return None, 200 return {"message": "ok"}, 200
@blueprint.route("/logout", methods=["POST"]) @blueprint.route("/logout", methods=["POST"])
@auth.requires_auth @auth.requires_auth
@endpoint.api()
def logout_user(): def logout_user():
auth.logout_current_user() auth.logout_current_user()
return None, 200 return {"message": "ok"}, 200
@blueprint.route("/social/<service>/authurl", methods=["GET"]) @blueprint.route("/social/<service>/authurl", methods=["GET"])
@auth.requires_auth @auth.requires_auth
@endpoint.api()
def get_user_social_auth_url(service): def get_user_social_auth_url(service):
try: try:
return {"url": get_social_login_url(service)} return {"url": get_social_login_url(service)}
@ -201,9 +201,9 @@ def get_user_social_auth_url(service):
@blueprint.route("/social/<service>/verify", methods=["POST"]) @blueprint.route("/social/<service>/verify", methods=["POST"])
@auth.requires_auth @auth.requires_auth
@endpoint.api( @body({
parameter('code', type=str, required=True) "code": fields.Str(required=True)
) })
def verify_user_social(service, code): def verify_user_social(service, code):
try: try:
# 1. verify with 3rd party # 1. verify with 3rd party
@ -227,22 +227,23 @@ def verify_user_social(service, code):
@blueprint.route("/recover", methods=["POST"]) @blueprint.route("/recover", methods=["POST"])
@endpoint.api( @body({
parameter('email', type=str, required=True) "email": fields.Str(required=True)
) })
def recover_user(email): def recover_user(email):
existing_user = User.get_by_email(email) existing_user = User.get_by_email(email)
if not existing_user: if not existing_user:
return {"message": "No user exists with that email"}, 400 return {"message": "No user exists with that email"}, 400
auth.throw_on_banned(existing_user) auth.throw_on_banned(existing_user)
existing_user.send_recovery_email() existing_user.send_recovery_email()
return None, 200 return {"message": "ok"}, 200
@blueprint.route("/recover/<code>", methods=["POST"]) @blueprint.route("/recover/<code>", methods=["POST"])
@endpoint.api( # TODO gaurd length
parameter('password', type=str, required=True), @body({
) "password": fields.Str(required=True)
})
def recover_email(code, password): def recover_email(code, password):
er = EmailRecovery.query.filter_by(code=code).first() er = EmailRecovery.query.filter_by(code=code).first()
if er: if er:
@ -252,16 +253,16 @@ def recover_email(code, password):
er.user.set_password(password) er.user.set_password(password)
db.session.delete(er) db.session.delete(er)
db.session.commit() db.session.commit()
return None, 200 return {"message": "ok"}, 200
return {"message": "Invalid reset code"}, 400 return {"message": "Invalid reset code"}, 400
@blueprint.route("/avatar", methods=["POST"]) @blueprint.route("/avatar", methods=["POST"])
@auth.requires_auth @auth.requires_auth
@endpoint.api( @body({
parameter('mimetype', type=str, required=True) "mimetype": fields.Str(required=True)
) })
def upload_avatar(mimetype): def upload_avatar(mimetype):
user = g.current_user user = g.current_user
try: try:
@ -273,9 +274,9 @@ def upload_avatar(mimetype):
@blueprint.route("/avatar", methods=["DELETE"]) @blueprint.route("/avatar", methods=["DELETE"])
@auth.requires_auth @auth.requires_auth
@endpoint.api( @body({
parameter('url', type=str, required=True) "url": fields.Str(required=True)
) })
def delete_avatar(url): def delete_avatar(url):
user = g.current_user user = g.current_user
remove_avatar(url, user.id) remove_avatar(url, user.id)
@ -284,12 +285,13 @@ def delete_avatar(url):
@blueprint.route("/<user_id>", methods=["PUT"]) @blueprint.route("/<user_id>", methods=["PUT"])
@auth.requires_auth @auth.requires_auth
@auth.requires_same_user_auth @auth.requires_same_user_auth
@endpoint.api( # TODO gaurd all (minimum, minimum, shape, uri)
parameter('displayName', type=str, required=True), @body({
parameter('title', type=str, required=True), "displayName": fields.Str(required=True),
parameter('socialMedias', type=list, required=True), "title": fields.Str(required=True),
parameter('avatar', type=str, required=True) "socialMedias": fields.List(fields.Dict(), required=True),
) "avatar": fields.Str(required=True)
})
def update_user(user_id, display_name, title, social_medias, avatar): def update_user(user_id, display_name, title, social_medias, avatar):
user = g.current_user user = g.current_user
@ -324,7 +326,6 @@ def update_user(user_id, display_name, title, social_medias, avatar):
@blueprint.route("/<user_id>/invites", methods=["GET"]) @blueprint.route("/<user_id>/invites", methods=["GET"])
@auth.requires_same_user_auth @auth.requires_same_user_auth
@endpoint.api()
def get_user_invites(user_id): def get_user_invites(user_id):
invites = ProposalTeamInvite.get_pending_for_user(g.current_user) invites = ProposalTeamInvite.get_pending_for_user(g.current_user)
return invites_with_proposal_schema.dump(invites) return invites_with_proposal_schema.dump(invites)
@ -332,9 +333,9 @@ def get_user_invites(user_id):
@blueprint.route("/<user_id>/invites/<invite_id>/respond", methods=["PUT"]) @blueprint.route("/<user_id>/invites/<invite_id>/respond", methods=["PUT"])
@auth.requires_same_user_auth @auth.requires_same_user_auth
@endpoint.api( @body({
parameter('response', type=bool, required=True) "response": fields.Bool(required=True)
) })
def respond_to_invite(user_id, invite_id, response): def respond_to_invite(user_id, invite_id, response):
invite = ProposalTeamInvite.query.filter_by(id=invite_id).first() invite = ProposalTeamInvite.query.filter_by(id=invite_id).first()
if not invite: if not invite:
@ -348,22 +349,22 @@ def respond_to_invite(user_id, invite_id, response):
db.session.add(invite) db.session.add(invite)
db.session.commit() db.session.commit()
return None, 200 return {"message": "ok"}, 200
@blueprint.route("/<user_id>/settings", methods=["GET"]) @blueprint.route("/<user_id>/settings", methods=["GET"])
@auth.requires_same_user_auth @auth.requires_same_user_auth
@endpoint.api()
def get_user_settings(user_id): def get_user_settings(user_id):
return user_settings_schema.dump(g.current_user.settings) return user_settings_schema.dump(g.current_user.settings)
@blueprint.route("/<user_id>/settings", methods=["PUT"]) @blueprint.route("/<user_id>/settings", methods=["PUT"])
@auth.requires_same_user_auth @auth.requires_same_user_auth
@endpoint.api( # TODO guard all (shape, validity)
parameter('emailSubscriptions', type=dict), @body({
parameter('refundAddress', type=str) "emailSubscriptions": fields.Dict(required=True),
) "refundAddress": fields.Str(required=False, missing=None)
})
def set_user_settings(user_id, email_subscriptions, refund_address): def set_user_settings(user_id, email_subscriptions, refund_address):
if email_subscriptions: if email_subscriptions:
try: try:
@ -381,9 +382,9 @@ def set_user_settings(user_id, email_subscriptions, refund_address):
@blueprint.route("/<user_id>/arbiter/<proposal_id>", methods=["PUT"]) @blueprint.route("/<user_id>/arbiter/<proposal_id>", methods=["PUT"])
@auth.requires_same_user_auth @auth.requires_same_user_auth
@endpoint.api( @body({
parameter('isAccept', type=bool) "isAccept": fields.Bool(required=False, missing=None)
) })
def set_user_arbiter(user_id, proposal_id, is_accept): def set_user_arbiter(user_id, proposal_id, is_accept):
try: try:
proposal = Proposal.query.filter_by(id=int(proposal_id)).first() proposal = Proposal.query.filter_by(id=int(proposal_id)).first()
@ -399,5 +400,3 @@ def set_user_arbiter(user_id, proposal_id, is_accept):
except ValidationException as e: except ValidationException as e:
return {"message": str(e)}, 400 return {"message": str(e)}, 400
return user_settings_schema.dump(g.current_user.settings)

View File

@ -53,9 +53,6 @@ markdownify
# email # email
sendgrid==5.6.0 sendgrid==5.6.0
# input validation
flask-yolo2API==0.2.6
#sentry #sentry
sentry-sdk[flask]==0.5.5 sentry-sdk[flask]==0.5.5
@ -71,5 +68,11 @@ Flask-Security==3.0.0
# oauth # oauth
requests-oauthlib==1.0.0 requests-oauthlib==1.0.0
# request parsing
webargs==5.1.2
# 2fa - totp # 2fa - totp
pyotp==2.2.7 pyotp==2.2.7
# JSON formatting
animal_case==0.4.1

View File

@ -72,9 +72,9 @@ class TestAdminAPI(BaseProposalCreatorConfig):
def assert_autherror(self, resp, contains): def assert_autherror(self, resp, contains):
# this should be 403 # this should be 403
self.assert500(resp) self.assert403(resp)
print(f'...check that [{resp.json["data"]}] contains [{contains}]') print(f'...check that [{resp.json["message"]}] contains [{contains}]')
self.assertTrue(contains in resp.json['data']) self.assertTrue(contains in resp.json['message'])
# happy path (mostly) # happy path (mostly)
def test_admin_2fa_setup_flow(self): def test_admin_2fa_setup_flow(self):
@ -245,22 +245,22 @@ class TestAdminAPI(BaseProposalCreatorConfig):
def test_update_proposal(self): def test_update_proposal(self):
self.login_admin() self.login_admin()
# set to 1 (on) # set to 1 (on)
resp_on = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data={"contributionMatching": 1}) resp_on = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data=json.dumps({"contributionMatching": 1}))
self.assert200(resp_on) self.assert200(resp_on)
self.assertEqual(resp_on.json['contributionMatching'], 1) self.assertEqual(resp_on.json['contributionMatching'], 1)
resp_off = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data={"contributionMatching": 0}) resp_off = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data=json.dumps({"contributionMatching": 0}))
self.assert200(resp_off) self.assert200(resp_off)
self.assertEqual(resp_off.json['contributionMatching'], 0) self.assertEqual(resp_off.json['contributionMatching'], 0)
def test_update_proposal_no_auth(self): def test_update_proposal_no_auth(self):
resp = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data={"contributionMatching": 1}) resp = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data=json.dumps({"contributionMatching": 1}))
self.assert401(resp) self.assert401(resp)
def test_update_proposal_bad_matching(self): def test_update_proposal_bad_matching(self):
self.login_admin() self.login_admin()
resp = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data={"contributionMatching": 2}) resp = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data=json.dumps({"contributionMatching": 2}))
self.assert500(resp) self.assert400(resp)
self.assertIn('Bad value', resp.json['data']) self.assertTrue(resp.json['message'])
@patch('requests.get', side_effect=mock_blockchain_api_requests) @patch('requests.get', side_effect=mock_blockchain_api_requests)
def test_approve_proposal(self, mock_get): def test_approve_proposal(self, mock_get):
@ -272,7 +272,7 @@ class TestAdminAPI(BaseProposalCreatorConfig):
# approve # approve
resp = self.app.put( resp = self.app.put(
"/api/v1/admin/proposals/{}/approve".format(self.proposal.id), "/api/v1/admin/proposals/{}/approve".format(self.proposal.id),
data={"isApprove": True} data=json.dumps({"isApprove": True})
) )
self.assert200(resp) self.assert200(resp)
self.assertEqual(resp.json["status"], ProposalStatus.APPROVED) self.assertEqual(resp.json["status"], ProposalStatus.APPROVED)
@ -287,7 +287,7 @@ class TestAdminAPI(BaseProposalCreatorConfig):
# reject # reject
resp = self.app.put( resp = self.app.put(
"/api/v1/admin/proposals/{}/approve".format(self.proposal.id), "/api/v1/admin/proposals/{}/approve".format(self.proposal.id),
data={"isApprove": False, "rejectReason": "Funnzies."} data=json.dumps({"isApprove": False, "rejectReason": "Funnzies."})
) )
self.assert200(resp) self.assert200(resp)
self.assertEqual(resp.json["status"], ProposalStatus.REJECTED) self.assertEqual(resp.json["status"], ProposalStatus.REJECTED)
@ -301,10 +301,43 @@ class TestAdminAPI(BaseProposalCreatorConfig):
# nominate arbiter # nominate arbiter
resp = self.app.put( resp = self.app.put(
"/api/v1/admin/arbiters", "/api/v1/admin/arbiters",
data={ data=json.dumps({
'proposalId': self.proposal.id, 'proposalId': self.proposal.id,
'userId': self.other_user.id 'userId': self.other_user.id
} })
) )
self.assert200(resp) self.assert200(resp)
# TODO - more tests # TODO - more tests
def test_create_rfp_succeeds(self):
self.login_admin()
resp = self.app.post(
"/api/v1/admin/rfps",
data=json.dumps({
"brief": "Some brief",
"category": "CORE_DEV",
"content": "CONTENT",
"dateCloses": 1553980004,
"status": "DRAFT",
"title": "TITLE"
})
)
self.assert200(resp)
def test_create_rfp_fails_with_bad_category(self):
self.login_admin()
resp = self.app.post(
"/api/v1/admin/rfps",
data=json.dumps({
"brief": "Some brief",
"category": "NOT_CORE_DEV",
"content": "CONTENT",
"dateCloses": 1553980004,
"status": "DRAFT",
"title": "TITLE"
})
)
self.assert400(resp)

View File

@ -45,7 +45,7 @@ class TestProposalAPI(BaseProposalCreatorConfig):
data=json.dumps(new_proposal), data=json.dumps(new_proposal),
content_type='application/json' content_type='application/json'
) )
print(resp)
self.assert200(resp) self.assert200(resp)
self.assertEqual(resp.json["title"], new_title) self.assertEqual(resp.json["title"], new_title)
self.assertEqual(self.proposal.title, new_title) self.assertEqual(self.proposal.title, new_title)

View File

@ -104,10 +104,10 @@ class TestUserAPI(BaseUserConfig):
}), }),
content_type="application/json" content_type="application/json"
) )
# self.assert403(user_auth_resp) self.assert403(user_auth_resp)
# self.assertTrue(user_auth_resp.json['message'] is not None) self.assertTrue(user_auth_resp.json['message'] is not None)
self.assert500(user_auth_resp) # self.assert500(user_auth_resp)
self.assertIn('Invalid pass', user_auth_resp.json['data']) # self.assertIn('Invalid pass', user_auth_resp.json['data'])
def test_user_auth_bad_email(self): def test_user_auth_bad_email(self):
user_auth_resp = self.app.post( user_auth_resp = self.app.post(
@ -118,10 +118,10 @@ class TestUserAPI(BaseUserConfig):
}), }),
content_type="application/json" content_type="application/json"
) )
# self.assert400(user_auth_resp) self.assert403(user_auth_resp)
# self.assertTrue(user_auth_resp.json['message'] is not None) self.assertTrue(user_auth_resp.json['message'] is not None)
self.assert500(user_auth_resp) # self.assert500(user_auth_resp)
self.assertIn('No user', user_auth_resp.json['data']) # self.assertIn('No user', user_auth_resp.json['data'])
def test_user_auth_banned(self): def test_user_auth_banned(self):
self.user.set_banned(True, 'reason for banning') self.user.set_banned(True, 'reason for banning')
@ -134,8 +134,8 @@ class TestUserAPI(BaseUserConfig):
content_type="application/json" content_type="application/json"
) )
# in test mode we get 500s instead of 403 # in test mode we get 500s instead of 403
self.assert500(user_auth_resp) self.assert403(user_auth_resp)
self.assertIn('banned', user_auth_resp.json['data']) self.assertIn('banned', user_auth_resp.json['message'])
def test_create_user_duplicate_400(self): def test_create_user_duplicate_400(self):
# self.user is identical to test_user, should throw # self.user is identical to test_user, should throw
@ -152,7 +152,7 @@ class TestUserAPI(BaseUserConfig):
self.login_default_user() self.login_default_user()
updated_user = animalify(copy.deepcopy(user_schema.dump(self.user))) updated_user = animalify(copy.deepcopy(user_schema.dump(self.user)))
updated_user["displayName"] = 'new display name' updated_user["displayName"] = 'new display name'
updated_user["avatar"] = {} updated_user["avatar"] = '' # TODO confirm avatar is no longer a dict
updated_user["socialMedias"] = [] updated_user["socialMedias"] = []
user_update_resp = self.app.put( user_update_resp = self.app.put(
@ -253,8 +253,9 @@ class TestUserAPI(BaseUserConfig):
content_type='application/json' content_type='application/json'
) )
# 404 outside testing mode # 404 outside testing mode
self.assertStatus(response, 500) self.assertStatus(response, 403)
self.assertIn('banned', response.json['data']) print(response.json)
self.assertIn('banned', response.json['message'])
def test_recover_user_no_user(self): def test_recover_user_no_user(self):
response = self.app.post( response = self.app.post(
@ -301,8 +302,8 @@ class TestUserAPI(BaseUserConfig):
content_type='application/json' content_type='application/json'
) )
# 403 outside of testing mode # 403 outside of testing mode
self.assertStatus(reset_resp, 500) self.assertStatus(reset_resp, 403)
self.assertIn('banned', reset_resp.json['data']) self.assertIn('banned', reset_resp.json['message'])
@patch('grant.user.views.verify_social') @patch('grant.user.views.verify_social')
def test_user_verify_social(self, mock_verify_social): def test_user_verify_social(self, mock_verify_social):