Webargs Request Parsing (#228)
This commit is contained in:
parent
f5d721c0b4
commit
f950e17397
|
@ -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 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.email.send import generate_email, send_email
|
||||
from grant.extensions import db
|
||||
from grant.milestone.models import Milestone
|
||||
from grant.parser import body, query, paginated_fields
|
||||
from grant.proposal.models import (
|
||||
Proposal,
|
||||
ProposalArbiter,
|
||||
|
@ -18,12 +23,11 @@ from grant.proposal.models import (
|
|||
admin_proposal_contribution_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
|
||||
import grant.utils.admin as admin
|
||||
import grant.utils.auth as auth
|
||||
from grant.utils.misc import make_url
|
||||
from grant.settings import EXPLORER_URL
|
||||
from grant.user.models import User, UserSettings, admin_users_schema, admin_user_schema
|
||||
from grant.utils import pagination
|
||||
from grant.utils.enums import Category
|
||||
from grant.utils.enums import (
|
||||
ProposalStatus,
|
||||
ProposalStage,
|
||||
|
@ -32,10 +36,7 @@ from grant.utils.enums import (
|
|||
MilestoneStage,
|
||||
RFPStatus,
|
||||
)
|
||||
from grant.utils import pagination
|
||||
from grant.settings import EXPLORER_URL
|
||||
from sqlalchemy import func, or_
|
||||
|
||||
from grant.utils.misc import make_url
|
||||
from .example_emails import example_email_args
|
||||
|
||||
blueprint = Blueprint('admin', __name__, url_prefix='/api/v1/admin')
|
||||
|
@ -59,16 +60,15 @@ def make_login_state():
|
|||
|
||||
|
||||
@blueprint.route("/checklogin", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def loggedin():
|
||||
return make_login_state()
|
||||
|
||||
|
||||
@blueprint.route("/login", methods=["POST"])
|
||||
@endpoint.api(
|
||||
parameter('username', type=str, required=False),
|
||||
parameter('password', type=str, required=False),
|
||||
)
|
||||
@body({
|
||||
"username": fields.Str(required=False, missing=None),
|
||||
"password": fields.Str(required=False, missing=None)
|
||||
})
|
||||
def login(username, password):
|
||||
if auth.auth_user(username, password):
|
||||
if admin.admin_is_authed():
|
||||
|
@ -77,9 +77,9 @@ def login(username, password):
|
|||
|
||||
|
||||
@blueprint.route("/refresh", methods=["POST"])
|
||||
@endpoint.api(
|
||||
parameter('password', type=str, required=True),
|
||||
)
|
||||
@body({
|
||||
"password": fields.Str(required=True)
|
||||
})
|
||||
def refresh(password):
|
||||
if auth.refresh_auth(password):
|
||||
return make_login_state()
|
||||
|
@ -88,7 +88,6 @@ def refresh(password):
|
|||
|
||||
|
||||
@blueprint.route("/2fa", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def get_2fa():
|
||||
if not admin.admin_is_authed():
|
||||
return {"message": "Must be authenticated"}, 403
|
||||
|
@ -96,18 +95,17 @@ def get_2fa():
|
|||
|
||||
|
||||
@blueprint.route("/2fa/init", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def get_2fa_init():
|
||||
admin.throw_on_2fa_not_allowed()
|
||||
return admin.make_2fa_setup()
|
||||
|
||||
|
||||
@blueprint.route("/2fa/enable", methods=["POST"])
|
||||
@endpoint.api(
|
||||
parameter('backupCodes', type=list, required=True),
|
||||
parameter('totpSecret', type=str, required=True),
|
||||
parameter('verifyCode', type=str, required=True),
|
||||
)
|
||||
@body({
|
||||
"backupCodes": fields.List(fields.Str(), required=True),
|
||||
"totpSecret": fields.Str(required=True),
|
||||
"verifyCode": fields.Str(required=True)
|
||||
})
|
||||
def post_2fa_enable(backup_codes, totp_secret, verify_code):
|
||||
admin.throw_on_2fa_not_allowed()
|
||||
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"])
|
||||
@endpoint.api(
|
||||
parameter('verifyCode', type=str, required=True),
|
||||
)
|
||||
@body({
|
||||
"verifyCode": fields.Str(required=True)
|
||||
})
|
||||
def post_2fa_verify(verify_code):
|
||||
admin.throw_on_2fa_not_allowed(allow_stale=True)
|
||||
admin.admin_auth_2fa(verify_code)
|
||||
|
@ -127,7 +125,6 @@ def post_2fa_verify(verify_code):
|
|||
|
||||
|
||||
@blueprint.route("/logout", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def logout():
|
||||
admin.logout()
|
||||
return {
|
||||
|
@ -137,7 +134,6 @@ def logout():
|
|||
|
||||
|
||||
@blueprint.route("/stats", methods=["GET"])
|
||||
@endpoint.api()
|
||||
@admin.admin_auth_required
|
||||
def stats():
|
||||
user_count = db.session.query(func.count(User.id)).scalar()
|
||||
|
@ -162,9 +158,9 @@ def stats():
|
|||
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
|
||||
.join(Proposal) \
|
||||
.filter(or_(
|
||||
Proposal.stage == ProposalStage.FAILED,
|
||||
Proposal.stage == ProposalStage.CANCELED,
|
||||
)) \
|
||||
Proposal.stage == ProposalStage.FAILED,
|
||||
Proposal.stage == ProposalStage.CANCELED,
|
||||
)) \
|
||||
.join(ProposalContribution.user) \
|
||||
.join(UserSettings) \
|
||||
.filter(UserSettings.refund_address != None) \
|
||||
|
@ -183,7 +179,6 @@ def stats():
|
|||
|
||||
|
||||
@blueprint.route('/users/<user_id>', methods=['DELETE'])
|
||||
@endpoint.api()
|
||||
@admin.admin_auth_required
|
||||
def delete_user(user_id):
|
||||
user = User.query.filter(User.id == user_id).first()
|
||||
|
@ -192,16 +187,11 @@ def delete_user(user_id):
|
|||
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
return None, 200
|
||||
return {"message": "ok"}, 200
|
||||
|
||||
|
||||
@blueprint.route("/users", methods=["GET"])
|
||||
@endpoint.api(
|
||||
parameter('page', type=int, required=False),
|
||||
parameter('filters', type=list, required=False),
|
||||
parameter('search', type=str, required=False),
|
||||
parameter('sort', type=str, required=False)
|
||||
)
|
||||
@query(paginated_fields)
|
||||
@admin.admin_auth_required
|
||||
def get_users(page, filters, search, sort):
|
||||
filters_workaround = request.args.getlist('filters[]')
|
||||
|
@ -217,7 +207,6 @@ def get_users(page, filters, search, sort):
|
|||
|
||||
|
||||
@blueprint.route('/users/<id>', methods=['GET'])
|
||||
@endpoint.api()
|
||||
@admin.admin_auth_required
|
||||
def get_user(id):
|
||||
user_db = User.query.filter(User.id == id).first()
|
||||
|
@ -235,12 +224,12 @@ def get_user(id):
|
|||
|
||||
|
||||
@blueprint.route('/users/<user_id>', methods=['PUT'])
|
||||
@endpoint.api(
|
||||
parameter('silenced', type=bool, required=False),
|
||||
parameter('banned', type=bool, required=False),
|
||||
parameter('bannedReason', type=str, required=False),
|
||||
parameter('isAdmin', type=bool, required=False)
|
||||
)
|
||||
@body({
|
||||
"silenced": fields.Bool(required=False, missing=None),
|
||||
"banned": fields.Bool(required=False, missing=None),
|
||||
"bannedReason": fields.Str(required=False, missing=None),
|
||||
"isAdmin": fields.Bool(required=False, missing=None),
|
||||
})
|
||||
@admin.admin_auth_required
|
||||
def edit_user(user_id, silenced, banned, banned_reason, is_admin):
|
||||
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"])
|
||||
@endpoint.api(
|
||||
parameter('search', type=str, required=False),
|
||||
)
|
||||
@query({
|
||||
"search": fields.Str(required=False, missing=None)
|
||||
})
|
||||
@admin.admin_auth_required
|
||||
def get_arbiters(search):
|
||||
results = []
|
||||
|
@ -289,10 +278,10 @@ def get_arbiters(search):
|
|||
|
||||
|
||||
@blueprint.route('/arbiters', methods=['PUT'])
|
||||
@endpoint.api(
|
||||
parameter('proposalId', type=int, required=True),
|
||||
parameter('userId', type=int, required=True)
|
||||
)
|
||||
@body({
|
||||
"proposalId": fields.Int(required=True),
|
||||
"userId": fields.Int(required=True),
|
||||
})
|
||||
@admin.admin_auth_required
|
||||
def set_arbiter(proposal_id, user_id):
|
||||
proposal = Proposal.query.filter(Proposal.id == proposal_id).first()
|
||||
|
@ -323,21 +312,16 @@ def set_arbiter(proposal_id, user_id):
|
|||
db.session.commit()
|
||||
|
||||
return {
|
||||
'proposal': proposal_schema.dump(proposal),
|
||||
'user': admin_user_schema.dump(user)
|
||||
}, 200
|
||||
'proposal': proposal_schema.dump(proposal),
|
||||
'user': admin_user_schema.dump(user)
|
||||
}, 200
|
||||
|
||||
|
||||
# PROPOSALS
|
||||
|
||||
|
||||
@blueprint.route("/proposals", methods=["GET"])
|
||||
@endpoint.api(
|
||||
parameter('page', type=int, required=False),
|
||||
parameter('filters', type=list, required=False),
|
||||
parameter('search', type=str, required=False),
|
||||
parameter('sort', type=str, required=False)
|
||||
)
|
||||
@query(paginated_fields)
|
||||
@admin.admin_auth_required
|
||||
def get_proposals(page, filters, search, sort):
|
||||
filters_workaround = request.args.getlist('filters[]')
|
||||
|
@ -353,7 +337,6 @@ def get_proposals(page, filters, search, sort):
|
|||
|
||||
|
||||
@blueprint.route('/proposals/<id>', methods=['GET'])
|
||||
@endpoint.api()
|
||||
@admin.admin_auth_required
|
||||
def get_proposal(id):
|
||||
proposal = Proposal.query.filter(Proposal.id == id).first()
|
||||
|
@ -363,16 +346,15 @@ def get_proposal(id):
|
|||
|
||||
|
||||
@blueprint.route('/proposals/<id>', methods=['DELETE'])
|
||||
@endpoint.api()
|
||||
@admin.admin_auth_required
|
||||
def delete_proposal(id):
|
||||
return {"message": "Not implemented."}, 400
|
||||
|
||||
|
||||
@blueprint.route('/proposals/<id>', methods=['PUT'])
|
||||
@endpoint.api(
|
||||
parameter('contributionMatching', type=float, required=False, default=None)
|
||||
)
|
||||
@body({
|
||||
"contributionMatching": fields.Int(required=False, missing=None)
|
||||
})
|
||||
@admin.admin_auth_required
|
||||
def update_proposal(id, contribution_matching):
|
||||
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'])
|
||||
@endpoint.api(
|
||||
parameter('isApprove', type=bool, required=True),
|
||||
parameter('rejectReason', type=str, required=False)
|
||||
)
|
||||
@body({
|
||||
"isApprove": fields.Bool(required=True),
|
||||
"rejectReason": fields.Str(required=False, missing=None)
|
||||
})
|
||||
@admin.admin_auth_required
|
||||
def approve_proposal(id, is_approve, reject_reason=None):
|
||||
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'])
|
||||
@endpoint.api()
|
||||
@admin.admin_auth_required
|
||||
def cancel_proposal(id):
|
||||
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"])
|
||||
@endpoint.api(
|
||||
parameter('txId', type=str, required=True),
|
||||
)
|
||||
@body({
|
||||
"txId": fields.Str(required=True),
|
||||
})
|
||||
@admin.admin_auth_required
|
||||
def paid_milestone_payout_request(id, mid, tx_id):
|
||||
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'])
|
||||
@endpoint.api()
|
||||
@admin.admin_auth_required
|
||||
def get_email_example(type):
|
||||
email = generate_email(type, example_email_args.get(type))
|
||||
|
@ -473,7 +453,6 @@ def get_email_example(type):
|
|||
|
||||
|
||||
@blueprint.route('/rfps', methods=['GET'])
|
||||
@endpoint.api()
|
||||
@admin.admin_auth_required
|
||||
def get_rfps():
|
||||
rfps = RFP.query.all()
|
||||
|
@ -481,15 +460,15 @@ def get_rfps():
|
|||
|
||||
|
||||
@blueprint.route('/rfps', methods=['POST'])
|
||||
@endpoint.api(
|
||||
parameter('title', type=str),
|
||||
parameter('brief', type=str),
|
||||
parameter('content', type=str),
|
||||
parameter('category', type=str),
|
||||
parameter('bounty', type=str),
|
||||
parameter('matching', type=bool, default=False),
|
||||
parameter('dateCloses', type=int),
|
||||
)
|
||||
@body({
|
||||
"title": fields.Str(required=True),
|
||||
"brief": fields.Str(required=True),
|
||||
"content": fields.Str(required=True),
|
||||
"category": fields.Str(required=True, validate=validate.OneOf(choices=Category.list())),
|
||||
"bounty": fields.Str(required=False, missing=0),
|
||||
"matching": fields.Bool(required=False, missing=False),
|
||||
"dateCloses": fields.Int(required=True)
|
||||
})
|
||||
@admin.admin_auth_required
|
||||
def create_rfp(date_closes, **kwargs):
|
||||
rfp = RFP(
|
||||
|
@ -498,11 +477,10 @@ def create_rfp(date_closes, **kwargs):
|
|||
)
|
||||
db.session.add(rfp)
|
||||
db.session.commit()
|
||||
return admin_rfp_schema.dump(rfp), 201
|
||||
return admin_rfp_schema.dump(rfp), 200
|
||||
|
||||
|
||||
@blueprint.route('/rfps/<rfp_id>', methods=['GET'])
|
||||
@endpoint.api()
|
||||
@admin.admin_auth_required
|
||||
def get_rfp(rfp_id):
|
||||
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'])
|
||||
@endpoint.api(
|
||||
parameter('title', type=str),
|
||||
parameter('brief', type=str),
|
||||
parameter('content', type=str),
|
||||
parameter('category', type=str),
|
||||
parameter('bounty', type=str),
|
||||
parameter('matching', type=bool, default=False),
|
||||
parameter('dateCloses', type=int),
|
||||
parameter('status', type=str),
|
||||
)
|
||||
@body({
|
||||
"title": fields.Str(required=True),
|
||||
"brief": fields.Str(required=True),
|
||||
"content": fields.Str(required=True),
|
||||
"category": fields.Str(required=True, validate=validate.OneOf(choices=Category.list())),
|
||||
"bounty": fields.Str(required=True),
|
||||
"matching": fields.Bool(required=True, default=False, missing=False),
|
||||
"dateCloses": fields.Int(required=True)
|
||||
})
|
||||
@admin.admin_auth_required
|
||||
def update_rfp(rfp_id, title, brief, content, category, bounty, matching, date_closes, status):
|
||||
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'])
|
||||
@endpoint.api()
|
||||
@admin.admin_auth_required
|
||||
def delete_rfp(rfp_id):
|
||||
rfp = RFP.query.filter(RFP.id == rfp_id).first()
|
||||
|
@ -561,19 +537,14 @@ def delete_rfp(rfp_id):
|
|||
|
||||
db.session.delete(rfp)
|
||||
db.session.commit()
|
||||
return None, 200
|
||||
return {"message": "ok"}, 200
|
||||
|
||||
|
||||
# Contributions
|
||||
|
||||
|
||||
@blueprint.route('/contributions', methods=['GET'])
|
||||
@endpoint.api(
|
||||
parameter('page', type=int, required=False),
|
||||
parameter('filters', type=list, required=False),
|
||||
parameter('search', type=str, required=False),
|
||||
parameter('sort', type=str, required=False)
|
||||
)
|
||||
@query(paginated_fields)
|
||||
@admin.admin_auth_required
|
||||
def get_contributions(page, filters, search, sort):
|
||||
filters_workaround = request.args.getlist('filters[]')
|
||||
|
@ -588,13 +559,14 @@ def get_contributions(page, filters, search, sort):
|
|||
|
||||
|
||||
@blueprint.route('/contributions', methods=['POST'])
|
||||
@endpoint.api(
|
||||
parameter('proposalId', type=int, required=True),
|
||||
parameter('userId', type=int, required=False, default=None),
|
||||
parameter('status', type=str, required=True),
|
||||
parameter('amount', type=str, required=True),
|
||||
parameter('txId', type=str, required=False),
|
||||
)
|
||||
@body({
|
||||
"proposalId": fields.Int(required=True),
|
||||
"userId": fields.Int(required=True),
|
||||
# TODO guard status
|
||||
"status": fields.Str(required=True),
|
||||
"amount": fields.Str(required=True),
|
||||
"txId": fields.Str(required=False, missing=None)
|
||||
})
|
||||
@admin.admin_auth_required
|
||||
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
|
||||
|
@ -603,6 +575,7 @@ def create_contribution(proposal_id, user_id, status, amount, tx_id):
|
|||
user_id=user_id,
|
||||
amount=amount,
|
||||
)
|
||||
# TODO guard status
|
||||
contribution.status = status
|
||||
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'])
|
||||
@endpoint.api()
|
||||
@admin.admin_auth_required
|
||||
def get_contribution(contribution_id):
|
||||
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'])
|
||||
@endpoint.api(
|
||||
parameter('proposalId', type=int, required=False),
|
||||
parameter('userId', type=int, required=False),
|
||||
parameter('status', type=str, required=False),
|
||||
parameter('amount', type=str, required=False),
|
||||
parameter('txId', type=str, required=False),
|
||||
parameter('refundTxId', type=str, required=False),
|
||||
)
|
||||
@body({
|
||||
"proposalId": fields.Int(required=False, missing=None),
|
||||
"userId": fields.Int(required=False, missing=None),
|
||||
# TODO guard status
|
||||
"status": fields.Str(required=False, missing=None),
|
||||
"amount": fields.Str(required=False, missing=None),
|
||||
"txId": fields.Str(required=False, missing=None)
|
||||
})
|
||||
@admin.admin_auth_required
|
||||
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()
|
||||
|
@ -694,12 +666,12 @@ def edit_contribution(contribution_id, proposal_id, user_id, status, amount, tx_
|
|||
|
||||
|
||||
@blueprint.route('/comments', methods=['GET'])
|
||||
@endpoint.api(
|
||||
parameter('page', type=int, required=False),
|
||||
parameter('filters', type=list, required=False),
|
||||
parameter('search', type=str, required=False),
|
||||
parameter('sort', type=str, required=False)
|
||||
)
|
||||
@body({
|
||||
"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),
|
||||
})
|
||||
@admin.admin_auth_required
|
||||
def get_comments(page, filters, search, sort):
|
||||
filters_workaround = request.args.getlist('filters[]')
|
||||
|
@ -714,10 +686,11 @@ def get_comments(page, filters, search, sort):
|
|||
|
||||
|
||||
@blueprint.route('/comments/<comment_id>', methods=['PUT'])
|
||||
@endpoint.api(
|
||||
parameter('hidden', type=bool, required=False),
|
||||
parameter('reported', type=bool, required=False),
|
||||
)
|
||||
@body({
|
||||
"hidden": fields.Bool(required=False, missing=None),
|
||||
"reported": fields.Bool(required=False, missing=None),
|
||||
|
||||
})
|
||||
@admin.admin_auth_required
|
||||
def edit_comment(comment_id, hidden, reported):
|
||||
comment = Comment.query.filter(Comment.id == comment_id).first()
|
||||
|
|
|
@ -1,19 +1,54 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""The app module, containing the app factory function."""
|
||||
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_security import SQLAlchemyUserDatastore
|
||||
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.extensions import bcrypt, migrate, db, ma, security
|
||||
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.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"]):
|
||||
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:
|
||||
app.config.from_object(conf)
|
||||
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.clean)
|
||||
app.cli.add_command(commands.urls)
|
||||
|
||||
app.cli.add_command(proposal.commands.create_proposal)
|
||||
app.cli.add_command(proposal.commands.create_proposals)
|
||||
app.cli.add_command(user.commands.delete_user)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from flask import Blueprint
|
||||
from flask_yoloapi import endpoint
|
||||
|
||||
from grant.blockchain.bootstrap import send_bootstrap_data
|
||||
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"])
|
||||
@internal_webhook
|
||||
@endpoint.api()
|
||||
def get_bootstrap_info():
|
||||
print('Bootstrap data requested from blockchain watcher microservice...')
|
||||
send_bootstrap_data()
|
||||
|
|
|
@ -1,14 +1,4 @@
|
|||
from flask import Blueprint
|
||||
from flask_yoloapi import endpoint
|
||||
|
||||
from .models import Comment, comments_schema
|
||||
|
||||
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
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
from flask import Blueprint
|
||||
from flask_yoloapi import endpoint
|
||||
|
||||
from .models import EmailVerification, db
|
||||
from grant.utils.enums import ProposalArbiterStatus
|
||||
|
||||
blueprint = Blueprint("email", __name__, url_prefix="/api/v1/email")
|
||||
|
||||
|
||||
@blueprint.route("/<code>/verify", methods=["POST"])
|
||||
@endpoint.api()
|
||||
def verify_email(code):
|
||||
ev = EmailVerification.query.filter_by(code=code).first()
|
||||
if ev:
|
||||
|
@ -20,7 +17,6 @@ def verify_email(code):
|
|||
|
||||
|
||||
@blueprint.route("/<code>/unsubscribe", methods=["POST"])
|
||||
@endpoint.api()
|
||||
def unsubscribe_email(code):
|
||||
ev = EmailVerification.query.filter_by(code=code).first()
|
||||
if ev:
|
||||
|
@ -32,7 +28,6 @@ def unsubscribe_email(code):
|
|||
|
||||
|
||||
@blueprint.route("/<code>/arbiter/<proposal_id>", methods=["POST"])
|
||||
@endpoint.api()
|
||||
def accept_arbiter(code, proposal_id):
|
||||
ev = EmailVerification.query.filter_by(code=code).first()
|
||||
if ev:
|
||||
|
|
|
@ -1,14 +1,4 @@
|
|||
from flask import Blueprint
|
||||
from flask_yoloapi import endpoint
|
||||
|
||||
from .models import Milestone, milestones_schema
|
||||
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -1,13 +1,19 @@
|
|||
from dateutil.parser import parse
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
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.email.send import send_email
|
||||
from grant.milestone.models import Milestone
|
||||
from grant.settings import EXPLORER_URL, PROPOSAL_STAKING_AMOUNT
|
||||
from grant.user.models import User
|
||||
from grant.parser import body, query, paginated_fields
|
||||
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 (
|
||||
requires_auth,
|
||||
requires_team_member_auth,
|
||||
|
@ -16,14 +22,10 @@ from grant.utils.auth import (
|
|||
get_authed_user,
|
||||
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.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 (
|
||||
Proposal,
|
||||
proposals_schema,
|
||||
|
@ -43,7 +45,6 @@ blueprint = Blueprint("proposal", __name__, url_prefix="/api/v1/proposals")
|
|||
|
||||
|
||||
@blueprint.route("/<proposal_id>", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def get_proposal(proposal_id):
|
||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||
if proposal:
|
||||
|
@ -60,12 +61,7 @@ def get_proposal(proposal_id):
|
|||
|
||||
|
||||
@blueprint.route("/<proposal_id>/comments", methods=["GET"])
|
||||
@endpoint.api(
|
||||
parameter('page', type=int, required=False),
|
||||
parameter('filters', type=list, required=False),
|
||||
parameter('search', type=str, required=False),
|
||||
parameter('sort', type=str, required=False)
|
||||
)
|
||||
@query(paginated_fields)
|
||||
def get_proposal_comments(proposal_id, page, filters, search, sort):
|
||||
# only using page, currently
|
||||
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"])
|
||||
@requires_email_verified_auth
|
||||
@endpoint.api()
|
||||
def report_proposal_comment(proposal_id, comment_id):
|
||||
# Make sure proposal exists
|
||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||
|
@ -95,15 +90,15 @@ def report_proposal_comment(proposal_id, comment_id):
|
|||
|
||||
comment.report(True)
|
||||
db.session.commit()
|
||||
return None, 200
|
||||
return {"message": "ok"}, 200
|
||||
|
||||
|
||||
@blueprint.route("/<proposal_id>/comments", methods=["POST"])
|
||||
@requires_email_verified_auth
|
||||
@endpoint.api(
|
||||
parameter('comment', type=str, required=True),
|
||||
parameter('parentCommentId', type=int, required=False)
|
||||
)
|
||||
@body({
|
||||
"comment": fields.Str(required=True),
|
||||
"parentCommentId": fields.Int(required=False, missing=None),
|
||||
})
|
||||
def post_proposal_comments(proposal_id, comment, parent_comment_id):
|
||||
# Make sure proposal exists
|
||||
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"])
|
||||
@endpoint.api(
|
||||
parameter('page', type=int, required=False),
|
||||
parameter('filters', type=list, required=False),
|
||||
parameter('search', type=str, required=False),
|
||||
parameter('sort', type=str, required=False)
|
||||
)
|
||||
@query(paginated_fields)
|
||||
def get_proposals(page, filters, search, sort):
|
||||
filters_workaround = request.args.getlist('filters[]')
|
||||
query = Proposal.query.filter_by(status=ProposalStatus.LIVE) \
|
||||
|
@ -185,9 +175,9 @@ def get_proposals(page, filters, search, sort):
|
|||
|
||||
@blueprint.route("/drafts", methods=["POST"])
|
||||
@requires_email_verified_auth
|
||||
@endpoint.api(
|
||||
parameter('rfpId', type=int),
|
||||
)
|
||||
@body({
|
||||
"rfpId": fields.Int(required=False, missing=None)
|
||||
})
|
||||
def make_proposal_draft(rfp_id):
|
||||
proposal = Proposal.create(status=ProposalStatus.DRAFT)
|
||||
proposal.team.append(g.current_user)
|
||||
|
@ -209,34 +199,34 @@ def make_proposal_draft(rfp_id):
|
|||
|
||||
@blueprint.route("/drafts", methods=["GET"])
|
||||
@requires_auth
|
||||
@endpoint.api()
|
||||
def get_proposal_drafts():
|
||||
proposals = (
|
||||
Proposal.query
|
||||
.filter(or_(
|
||||
.filter(or_(
|
||||
Proposal.status == ProposalStatus.DRAFT,
|
||||
Proposal.status == ProposalStatus.REJECTED,
|
||||
))
|
||||
.join(proposal_team)
|
||||
.filter(proposal_team.c.user_id == g.current_user.id)
|
||||
.order_by(Proposal.date_created.desc())
|
||||
.all()
|
||||
.join(proposal_team)
|
||||
.filter(proposal_team.c.user_id == g.current_user.id)
|
||||
.order_by(Proposal.date_created.desc())
|
||||
.all()
|
||||
)
|
||||
return proposals_schema.dump(proposals), 200
|
||||
|
||||
|
||||
@blueprint.route("/<proposal_id>", methods=["PUT"])
|
||||
@requires_team_member_auth
|
||||
@endpoint.api(
|
||||
parameter('title', type=str),
|
||||
parameter('brief', type=str),
|
||||
parameter('category', type=str),
|
||||
parameter('content', type=str),
|
||||
parameter('target', type=str),
|
||||
parameter('payoutAddress', type=str),
|
||||
parameter('deadlineDuration', type=int),
|
||||
parameter('milestones', type=list)
|
||||
)
|
||||
# TODO add gaurd (minimum, maximum, shape)
|
||||
@body({
|
||||
"title": fields.Str(required=True),
|
||||
"brief": fields.Str(required=True),
|
||||
"category": fields.Str(required=True, validate=validate.OneOf(choices=Category.list())),
|
||||
"content": fields.Str(required=True),
|
||||
"target": fields.Str(required=True),
|
||||
"payoutAddress": fields.Str(required=True),
|
||||
"deadlineDuration": fields.Int(required=True),
|
||||
"milestones": fields.List(fields.Dict(), required=True),
|
||||
})
|
||||
def update_proposal(milestones, proposal_id, **kwargs):
|
||||
# Update the base proposal fields
|
||||
try:
|
||||
|
@ -251,9 +241,9 @@ def update_proposal(milestones, proposal_id, **kwargs):
|
|||
m = Milestone(
|
||||
title=mdata["title"],
|
||||
content=mdata["content"],
|
||||
date_estimated=datetime.fromtimestamp(mdata["dateEstimated"]),
|
||||
payout_percent=str(mdata["payoutPercent"]),
|
||||
immediate_payout=mdata["immediatePayout"],
|
||||
date_estimated=datetime.fromtimestamp(mdata["date_estimated"]),
|
||||
payout_percent=str(mdata["payout_percent"]),
|
||||
immediate_payout=mdata["immediate_payout"],
|
||||
proposal_id=g.current_proposal.id,
|
||||
index=i
|
||||
)
|
||||
|
@ -266,7 +256,6 @@ def update_proposal(milestones, proposal_id, **kwargs):
|
|||
|
||||
@blueprint.route("/<proposal_id>/rfp", methods=["DELETE"])
|
||||
@requires_team_member_auth
|
||||
@endpoint.api()
|
||||
def unlink_proposal_from_rfp(proposal_id):
|
||||
g.current_proposal.rfp_id = None
|
||||
db.session.add(g.current_proposal)
|
||||
|
@ -276,7 +265,6 @@ def unlink_proposal_from_rfp(proposal_id):
|
|||
|
||||
@blueprint.route("/<proposal_id>", methods=["DELETE"])
|
||||
@requires_team_member_auth
|
||||
@endpoint.api()
|
||||
def delete_proposal(proposal_id):
|
||||
deleteable_statuses = [
|
||||
ProposalStatus.DRAFT,
|
||||
|
@ -290,12 +278,11 @@ def delete_proposal(proposal_id):
|
|||
return {"message": "Cannot delete proposals with %s status" % status}, 400
|
||||
db.session.delete(g.current_proposal)
|
||||
db.session.commit()
|
||||
return None, 202
|
||||
return {"message": "ok"}, 202
|
||||
|
||||
|
||||
@blueprint.route("/<proposal_id>/submit_for_approval", methods=["PUT"])
|
||||
@requires_team_member_auth
|
||||
@endpoint.api()
|
||||
def submit_for_approval_proposal(proposal_id):
|
||||
try:
|
||||
g.current_proposal.submit_for_approval()
|
||||
|
@ -308,19 +295,17 @@ def submit_for_approval_proposal(proposal_id):
|
|||
|
||||
@blueprint.route("/<proposal_id>/stake", methods=["GET"])
|
||||
@requires_team_member_auth
|
||||
@endpoint.api()
|
||||
def get_proposal_stake(proposal_id):
|
||||
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)
|
||||
if contribution:
|
||||
return proposal_contribution_schema.dump(contribution)
|
||||
return None, 404
|
||||
return {"message": "ok"}, 404
|
||||
|
||||
|
||||
@blueprint.route("/<proposal_id>/publish", methods=["PUT"])
|
||||
@requires_team_member_auth
|
||||
@endpoint.api()
|
||||
def publish_proposal(proposal_id):
|
||||
try:
|
||||
g.current_proposal.publish()
|
||||
|
@ -336,7 +321,6 @@ def publish_proposal(proposal_id):
|
|||
|
||||
|
||||
@blueprint.route("/<proposal_id>/updates", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def get_proposal_updates(proposal_id):
|
||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||
if proposal:
|
||||
|
@ -347,7 +331,6 @@ def get_proposal_updates(proposal_id):
|
|||
|
||||
|
||||
@blueprint.route("/<proposal_id>/updates/<update_id>", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def get_proposal_update(proposal_id, update_id):
|
||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||
if proposal:
|
||||
|
@ -362,10 +345,10 @@ def get_proposal_update(proposal_id, update_id):
|
|||
|
||||
@blueprint.route("/<proposal_id>/updates", methods=["POST"])
|
||||
@requires_team_member_auth
|
||||
@endpoint.api(
|
||||
parameter('title', type=str, required=True),
|
||||
parameter('content', type=str, required=True)
|
||||
)
|
||||
@body({
|
||||
"title": fields.Str(required=True),
|
||||
"content": fields.Str(required=True)
|
||||
})
|
||||
def post_proposal_update(proposal_id, title, content):
|
||||
update = ProposalUpdate(
|
||||
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"])
|
||||
@requires_team_member_auth
|
||||
@endpoint.api(
|
||||
parameter('address', type=str, required=True)
|
||||
)
|
||||
@body({
|
||||
"address": fields.Str(required=True),
|
||||
})
|
||||
def post_proposal_team_invite(proposal_id, address):
|
||||
invite = ProposalTeamInvite(
|
||||
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"])
|
||||
@requires_team_member_auth
|
||||
@endpoint.api()
|
||||
def delete_proposal_team_invite(proposal_id, id_or_address):
|
||||
invite = ProposalTeamInvite.query.filter(
|
||||
(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.commit()
|
||||
return None, 202
|
||||
return {"message": "ok"}, 202
|
||||
|
||||
|
||||
@blueprint.route("/<proposal_id>/contributions", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def get_proposal_contributions(proposal_id):
|
||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||
if not proposal:
|
||||
|
@ -447,19 +428,19 @@ def get_proposal_contributions(proposal_id):
|
|||
|
||||
top_contributions = ProposalContribution.query \
|
||||
.filter_by(
|
||||
proposal_id=proposal_id,
|
||||
status=ContributionStatus.CONFIRMED,
|
||||
staking=False,
|
||||
) \
|
||||
proposal_id=proposal_id,
|
||||
status=ContributionStatus.CONFIRMED,
|
||||
staking=False,
|
||||
) \
|
||||
.order_by(ProposalContribution.amount.desc()) \
|
||||
.limit(5) \
|
||||
.all()
|
||||
latest_contributions = ProposalContribution.query \
|
||||
.filter_by(
|
||||
proposal_id=proposal_id,
|
||||
status=ContributionStatus.CONFIRMED,
|
||||
staking=False,
|
||||
) \
|
||||
proposal_id=proposal_id,
|
||||
status=ContributionStatus.CONFIRMED,
|
||||
staking=False,
|
||||
) \
|
||||
.order_by(ProposalContribution.date_created.desc()) \
|
||||
.limit(5) \
|
||||
.all()
|
||||
|
@ -471,7 +452,6 @@ def get_proposal_contributions(proposal_id):
|
|||
|
||||
|
||||
@blueprint.route("/<proposal_id>/contributions/<contribution_id>", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def get_proposal_contribution(proposal_id, contribution_id):
|
||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||
if proposal:
|
||||
|
@ -485,10 +465,11 @@ def get_proposal_contribution(proposal_id, contribution_id):
|
|||
|
||||
|
||||
@blueprint.route("/<proposal_id>/contributions", methods=["POST"])
|
||||
@endpoint.api(
|
||||
parameter('amount', type=str, required=True),
|
||||
parameter('anonymous', type=bool, required=False)
|
||||
)
|
||||
# TODO add gaurd (minimum, maximum)
|
||||
@body({
|
||||
"amount": fields.Str(required=True),
|
||||
"anonymous": fields.Bool(required=False, missing=None)
|
||||
})
|
||||
def post_proposal_contribution(proposal_id, amount, anonymous):
|
||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||
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
|
||||
@blueprint.route("/contribution/<contribution_id>/confirm", methods=["POST"])
|
||||
@internal_webhook
|
||||
@endpoint.api(
|
||||
parameter('to', type=str, required=True),
|
||||
parameter('amount', type=str, required=True),
|
||||
parameter('txid', type=str, required=True),
|
||||
)
|
||||
@body({
|
||||
"to": fields.Str(required=True),
|
||||
"amount": fields.Str(required=True),
|
||||
"txid": fields.Str(required=True),
|
||||
})
|
||||
def post_contribution_confirmation(contribution_id, to, amount, txid):
|
||||
contribution = ProposalContribution.query.filter_by(
|
||||
id=contribution_id).first()
|
||||
|
@ -534,7 +515,7 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
|
|||
|
||||
if contribution.status == ContributionStatus.CONFIRMED:
|
||||
# Duplicates can happen, just return ok
|
||||
return None, 200
|
||||
return {"message": "ok"}, 200
|
||||
|
||||
# Convert to whole zcash coins from zats
|
||||
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()
|
||||
|
||||
db.session.commit()
|
||||
return None, 200
|
||||
return {"message": "ok"}, 200
|
||||
|
||||
|
||||
@blueprint.route("/contribution/<contribution_id>", methods=["DELETE"])
|
||||
@requires_auth
|
||||
@endpoint.api()
|
||||
def delete_proposal_contribution(contribution_id):
|
||||
contribution = contribution = ProposalContribution.query.filter_by(
|
||||
contribution = ProposalContribution.query.filter_by(
|
||||
id=contribution_id).first()
|
||||
if not contribution:
|
||||
return {"message": "No contribution matching id"}, 404
|
||||
|
@ -602,13 +582,12 @@ def delete_proposal_contribution(contribution_id):
|
|||
contribution.status = ContributionStatus.DELETED
|
||||
db.session.add(contribution)
|
||||
db.session.commit()
|
||||
return None, 202
|
||||
return {"message": "ok"}, 202
|
||||
|
||||
|
||||
# request MS payout
|
||||
@blueprint.route("/<proposal_id>/milestone/<milestone_id>/request", methods=["PUT"])
|
||||
@requires_team_member_auth
|
||||
@endpoint.api()
|
||||
def request_milestone_payout(proposal_id, milestone_id):
|
||||
if not g.current_proposal.is_funded:
|
||||
return {"message": "Proposal is not fully funded"}, 400
|
||||
|
@ -630,7 +609,6 @@ def request_milestone_payout(proposal_id, milestone_id):
|
|||
# accept MS payout (arbiter)
|
||||
@blueprint.route("/<proposal_id>/milestone/<milestone_id>/accept", methods=["PUT"])
|
||||
@requires_arbiter_auth
|
||||
@endpoint.api()
|
||||
def accept_milestone_payout_request(proposal_id, milestone_id):
|
||||
if not g.current_proposal.is_funded:
|
||||
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)
|
||||
@blueprint.route("/<proposal_id>/milestone/<milestone_id>/reject", methods=["PUT"])
|
||||
@requires_arbiter_auth
|
||||
@endpoint.api(
|
||||
parameter('reason', type=str, required=True),
|
||||
)
|
||||
@body({
|
||||
"reason": fields.Str(required=True)
|
||||
})
|
||||
def reject_milestone_payout_request(proposal_id, milestone_id, reason):
|
||||
if not g.current_proposal.is_funded:
|
||||
return {"message": "Proposal is not fully funded"}, 400
|
||||
|
|
|
@ -2,6 +2,7 @@ from datetime import datetime
|
|||
from grant.extensions import ma, db
|
||||
from grant.utils.enums import RFPStatus
|
||||
from grant.utils.misc import dt_to_unix
|
||||
from grant.utils.enums import Category
|
||||
|
||||
|
||||
class RFP(db.Model):
|
||||
|
@ -46,6 +47,8 @@ class RFP(db.Model):
|
|||
matching: bool = False,
|
||||
status: str = RFPStatus.DRAFT,
|
||||
):
|
||||
# TODO add status assert
|
||||
assert Category.includes(category)
|
||||
self.date_created = datetime.now()
|
||||
self.title = title
|
||||
self.brief = brief
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from flask import Blueprint, g
|
||||
from flask_yoloapi import endpoint, parameter
|
||||
from flask import Blueprint
|
||||
from sqlalchemy import or_
|
||||
|
||||
from grant.utils.enums import RFPStatus
|
||||
|
@ -9,20 +8,18 @@ blueprint = Blueprint("rfp", __name__, url_prefix="/api/v1/rfps")
|
|||
|
||||
|
||||
@blueprint.route("/", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def get_rfps():
|
||||
rfps = RFP.query \
|
||||
.filter(or_(
|
||||
RFP.status == RFPStatus.LIVE,
|
||||
RFP.status == RFPStatus.CLOSED,
|
||||
)) \
|
||||
RFP.status == RFPStatus.LIVE,
|
||||
RFP.status == RFPStatus.CLOSED,
|
||||
)) \
|
||||
.order_by(RFP.date_created.desc()) \
|
||||
.all()
|
||||
return rfps_schema.dump(rfps)
|
||||
|
||||
|
||||
@blueprint.route("/<rfp_id>", methods=["GET"])
|
||||
@endpoint.api()
|
||||
def get_rfp(rfp_id):
|
||||
rfp = RFP.query.filter_by(id=rfp_id).first()
|
||||
if not rfp or rfp.status == RFPStatus.DRAFT:
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
from animal_case import keys_to_snake_case
|
||||
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.email.models import EmailRecovery
|
||||
from grant.parser import query, body
|
||||
from grant.proposal.models import (
|
||||
Proposal,
|
||||
proposal_team,
|
||||
|
@ -13,12 +16,10 @@ from grant.proposal.models import (
|
|||
user_proposals_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.social import verify_social, get_social_login_url, VerifySocialException
|
||||
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 (
|
||||
User,
|
||||
SocialMedia,
|
||||
|
@ -34,9 +35,9 @@ blueprint = Blueprint('user', __name__, url_prefix='/api/v1/users')
|
|||
|
||||
|
||||
@blueprint.route("/", methods=["GET"])
|
||||
@endpoint.api(
|
||||
parameter('proposalId', type=str, required=False)
|
||||
)
|
||||
@query({
|
||||
"proposalId": fields.Str(required=False, missing=None)
|
||||
})
|
||||
def get_users(proposal_id):
|
||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||
if not proposal:
|
||||
|
@ -44,10 +45,10 @@ def get_users(proposal_id):
|
|||
else:
|
||||
users = (
|
||||
User.query
|
||||
.join(proposal_team)
|
||||
.join(Proposal)
|
||||
.filter(proposal_team.c.proposal_id == proposal.id)
|
||||
.all()
|
||||
.join(proposal_team)
|
||||
.join(Proposal)
|
||||
.filter(proposal_team.c.proposal_id == proposal.id)
|
||||
.all()
|
||||
)
|
||||
result = users_schema.dump(users)
|
||||
return result
|
||||
|
@ -55,20 +56,19 @@ def get_users(proposal_id):
|
|||
|
||||
@blueprint.route("/me", methods=["GET"])
|
||||
@auth.requires_auth
|
||||
@endpoint.api()
|
||||
def get_me():
|
||||
dumped_user = self_user_schema.dump(g.current_user)
|
||||
return dumped_user
|
||||
|
||||
|
||||
@blueprint.route("/<user_id>", methods=["GET"])
|
||||
@endpoint.api(
|
||||
parameter("withProposals", type=bool, required=False),
|
||||
parameter("withComments", type=bool, required=False),
|
||||
parameter("withFunded", type=bool, required=False),
|
||||
parameter("withPending", type=bool, required=False),
|
||||
parameter("withArbitrated", type=bool, required=False)
|
||||
)
|
||||
@query({
|
||||
"withProposals": fields.Bool(required=False, missing=None),
|
||||
"withComments": fields.Bool(required=False, missing=None),
|
||||
"withFunded": fields.Bool(required=False, missing=None),
|
||||
"withPending": fields.Bool(required=False, missing=None),
|
||||
"withArbitrated": fields.Bool(required=False, missing=None)
|
||||
})
|
||||
def get_user(user_id, with_proposals, with_comments, with_funded, with_pending, with_arbitrated):
|
||||
user = User.get_by_id(user_id)
|
||||
if user:
|
||||
|
@ -109,12 +109,13 @@ def get_user(user_id, with_proposals, with_comments, with_funded, with_pending,
|
|||
|
||||
|
||||
@blueprint.route("/", methods=["POST"])
|
||||
@endpoint.api(
|
||||
parameter('emailAddress', type=str, required=True),
|
||||
parameter('password', type=str, required=True),
|
||||
parameter('displayName', type=str, required=True),
|
||||
parameter('title', type=str, required=True)
|
||||
)
|
||||
@body({
|
||||
# TODO guard all (valid, minimum, maximum)
|
||||
"emailAddress": fields.Str(required=True),
|
||||
"password": fields.Str(required=True),
|
||||
"displayName": fields.Str(required=True),
|
||||
"title": fields.Str(required=True),
|
||||
})
|
||||
def create_user(
|
||||
email_address,
|
||||
password,
|
||||
|
@ -137,10 +138,10 @@ def create_user(
|
|||
|
||||
|
||||
@blueprint.route("/auth", methods=["POST"])
|
||||
@endpoint.api(
|
||||
parameter('email', type=str, required=True),
|
||||
parameter('password', type=str, required=True)
|
||||
)
|
||||
@body({
|
||||
"email": fields.Str(required=True),
|
||||
"password": fields.Str(required=True)
|
||||
})
|
||||
def auth_user(email, password):
|
||||
authed_user = auth.auth_user(email, password)
|
||||
return self_user_schema.dump(authed_user)
|
||||
|
@ -148,49 +149,48 @@ def auth_user(email, password):
|
|||
|
||||
@blueprint.route("/me/password", methods=["PUT"])
|
||||
@auth.requires_auth
|
||||
@endpoint.api(
|
||||
parameter('currentPassword', type=str, required=True),
|
||||
parameter('password', type=str, required=True),
|
||||
)
|
||||
# TODO gaurd password (minimum)
|
||||
@body({
|
||||
"currentPassword": fields.Str(required=True),
|
||||
"password": fields.Str(required=True)
|
||||
})
|
||||
def update_user_password(current_password, password):
|
||||
if not g.current_user.check_password(current_password):
|
||||
return {"message": "Current password incorrect"}, 403
|
||||
g.current_user.set_password(password)
|
||||
return None, 200
|
||||
return {"message": "ok"}, 200
|
||||
|
||||
|
||||
@blueprint.route("/me/email", methods=["PUT"])
|
||||
@auth.requires_auth
|
||||
@endpoint.api(
|
||||
parameter('email', type=str, required=True),
|
||||
parameter('password', type=str, required=True)
|
||||
)
|
||||
# TODO gaurd all (valid, minimum)
|
||||
@body({
|
||||
"email": fields.Str(required=True),
|
||||
"password": fields.Str(required=True)
|
||||
})
|
||||
def update_user_email(email, password):
|
||||
if not g.current_user.check_password(password):
|
||||
return {"message": "Password is incorrect"}, 403
|
||||
g.current_user.set_email(email)
|
||||
return None, 200
|
||||
return {"message": "ok"}, 200
|
||||
|
||||
|
||||
@blueprint.route("/me/resend-verification", methods=["PUT"])
|
||||
@auth.requires_auth
|
||||
@endpoint.api()
|
||||
def resend_email_verification():
|
||||
g.current_user.send_verification_email()
|
||||
return None, 200
|
||||
return {"message": "ok"}, 200
|
||||
|
||||
|
||||
@blueprint.route("/logout", methods=["POST"])
|
||||
@auth.requires_auth
|
||||
@endpoint.api()
|
||||
def logout_user():
|
||||
auth.logout_current_user()
|
||||
return None, 200
|
||||
return {"message": "ok"}, 200
|
||||
|
||||
|
||||
@blueprint.route("/social/<service>/authurl", methods=["GET"])
|
||||
@auth.requires_auth
|
||||
@endpoint.api()
|
||||
def get_user_social_auth_url(service):
|
||||
try:
|
||||
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"])
|
||||
@auth.requires_auth
|
||||
@endpoint.api(
|
||||
parameter('code', type=str, required=True)
|
||||
)
|
||||
@body({
|
||||
"code": fields.Str(required=True)
|
||||
})
|
||||
def verify_user_social(service, code):
|
||||
try:
|
||||
# 1. verify with 3rd party
|
||||
|
@ -227,22 +227,23 @@ def verify_user_social(service, code):
|
|||
|
||||
|
||||
@blueprint.route("/recover", methods=["POST"])
|
||||
@endpoint.api(
|
||||
parameter('email', type=str, required=True)
|
||||
)
|
||||
@body({
|
||||
"email": fields.Str(required=True)
|
||||
})
|
||||
def recover_user(email):
|
||||
existing_user = User.get_by_email(email)
|
||||
if not existing_user:
|
||||
return {"message": "No user exists with that email"}, 400
|
||||
auth.throw_on_banned(existing_user)
|
||||
existing_user.send_recovery_email()
|
||||
return None, 200
|
||||
return {"message": "ok"}, 200
|
||||
|
||||
|
||||
@blueprint.route("/recover/<code>", methods=["POST"])
|
||||
@endpoint.api(
|
||||
parameter('password', type=str, required=True),
|
||||
)
|
||||
# TODO gaurd length
|
||||
@body({
|
||||
"password": fields.Str(required=True)
|
||||
})
|
||||
def recover_email(code, password):
|
||||
er = EmailRecovery.query.filter_by(code=code).first()
|
||||
if er:
|
||||
|
@ -252,16 +253,16 @@ def recover_email(code, password):
|
|||
er.user.set_password(password)
|
||||
db.session.delete(er)
|
||||
db.session.commit()
|
||||
return None, 200
|
||||
return {"message": "ok"}, 200
|
||||
|
||||
return {"message": "Invalid reset code"}, 400
|
||||
|
||||
|
||||
@blueprint.route("/avatar", methods=["POST"])
|
||||
@auth.requires_auth
|
||||
@endpoint.api(
|
||||
parameter('mimetype', type=str, required=True)
|
||||
)
|
||||
@body({
|
||||
"mimetype": fields.Str(required=True)
|
||||
})
|
||||
def upload_avatar(mimetype):
|
||||
user = g.current_user
|
||||
try:
|
||||
|
@ -273,9 +274,9 @@ def upload_avatar(mimetype):
|
|||
|
||||
@blueprint.route("/avatar", methods=["DELETE"])
|
||||
@auth.requires_auth
|
||||
@endpoint.api(
|
||||
parameter('url', type=str, required=True)
|
||||
)
|
||||
@body({
|
||||
"url": fields.Str(required=True)
|
||||
})
|
||||
def delete_avatar(url):
|
||||
user = g.current_user
|
||||
remove_avatar(url, user.id)
|
||||
|
@ -284,12 +285,13 @@ def delete_avatar(url):
|
|||
@blueprint.route("/<user_id>", methods=["PUT"])
|
||||
@auth.requires_auth
|
||||
@auth.requires_same_user_auth
|
||||
@endpoint.api(
|
||||
parameter('displayName', type=str, required=True),
|
||||
parameter('title', type=str, required=True),
|
||||
parameter('socialMedias', type=list, required=True),
|
||||
parameter('avatar', type=str, required=True)
|
||||
)
|
||||
# TODO gaurd all (minimum, minimum, shape, uri)
|
||||
@body({
|
||||
"displayName": fields.Str(required=True),
|
||||
"title": fields.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):
|
||||
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"])
|
||||
@auth.requires_same_user_auth
|
||||
@endpoint.api()
|
||||
def get_user_invites(user_id):
|
||||
invites = ProposalTeamInvite.get_pending_for_user(g.current_user)
|
||||
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"])
|
||||
@auth.requires_same_user_auth
|
||||
@endpoint.api(
|
||||
parameter('response', type=bool, required=True)
|
||||
)
|
||||
@body({
|
||||
"response": fields.Bool(required=True)
|
||||
})
|
||||
def respond_to_invite(user_id, invite_id, response):
|
||||
invite = ProposalTeamInvite.query.filter_by(id=invite_id).first()
|
||||
if not invite:
|
||||
|
@ -348,22 +349,22 @@ def respond_to_invite(user_id, invite_id, response):
|
|||
db.session.add(invite)
|
||||
|
||||
db.session.commit()
|
||||
return None, 200
|
||||
return {"message": "ok"}, 200
|
||||
|
||||
|
||||
@blueprint.route("/<user_id>/settings", methods=["GET"])
|
||||
@auth.requires_same_user_auth
|
||||
@endpoint.api()
|
||||
def get_user_settings(user_id):
|
||||
return user_settings_schema.dump(g.current_user.settings)
|
||||
|
||||
|
||||
@blueprint.route("/<user_id>/settings", methods=["PUT"])
|
||||
@auth.requires_same_user_auth
|
||||
@endpoint.api(
|
||||
parameter('emailSubscriptions', type=dict),
|
||||
parameter('refundAddress', type=str)
|
||||
)
|
||||
# TODO guard all (shape, validity)
|
||||
@body({
|
||||
"emailSubscriptions": fields.Dict(required=True),
|
||||
"refundAddress": fields.Str(required=False, missing=None)
|
||||
})
|
||||
def set_user_settings(user_id, email_subscriptions, refund_address):
|
||||
if email_subscriptions:
|
||||
try:
|
||||
|
@ -381,9 +382,9 @@ def set_user_settings(user_id, email_subscriptions, refund_address):
|
|||
|
||||
@blueprint.route("/<user_id>/arbiter/<proposal_id>", methods=["PUT"])
|
||||
@auth.requires_same_user_auth
|
||||
@endpoint.api(
|
||||
parameter('isAccept', type=bool)
|
||||
)
|
||||
@body({
|
||||
"isAccept": fields.Bool(required=False, missing=None)
|
||||
})
|
||||
def set_user_arbiter(user_id, proposal_id, is_accept):
|
||||
try:
|
||||
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:
|
||||
return {"message": str(e)}, 400
|
||||
|
||||
return user_settings_schema.dump(g.current_user.settings)
|
||||
|
|
|
@ -53,9 +53,6 @@ markdownify
|
|||
# email
|
||||
sendgrid==5.6.0
|
||||
|
||||
# input validation
|
||||
flask-yolo2API==0.2.6
|
||||
|
||||
#sentry
|
||||
sentry-sdk[flask]==0.5.5
|
||||
|
||||
|
@ -71,5 +68,11 @@ Flask-Security==3.0.0
|
|||
# oauth
|
||||
requests-oauthlib==1.0.0
|
||||
|
||||
# request parsing
|
||||
webargs==5.1.2
|
||||
|
||||
# 2fa - totp
|
||||
pyotp==2.2.7
|
||||
|
||||
# JSON formatting
|
||||
animal_case==0.4.1
|
|
@ -72,9 +72,9 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
|||
|
||||
def assert_autherror(self, resp, contains):
|
||||
# this should be 403
|
||||
self.assert500(resp)
|
||||
print(f'...check that [{resp.json["data"]}] contains [{contains}]')
|
||||
self.assertTrue(contains in resp.json['data'])
|
||||
self.assert403(resp)
|
||||
print(f'...check that [{resp.json["message"]}] contains [{contains}]')
|
||||
self.assertTrue(contains in resp.json['message'])
|
||||
|
||||
# happy path (mostly)
|
||||
def test_admin_2fa_setup_flow(self):
|
||||
|
@ -245,22 +245,22 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
|||
def test_update_proposal(self):
|
||||
self.login_admin()
|
||||
# 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.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.assertEqual(resp_off.json['contributionMatching'], 0)
|
||||
|
||||
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)
|
||||
|
||||
def test_update_proposal_bad_matching(self):
|
||||
self.login_admin()
|
||||
resp = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data={"contributionMatching": 2})
|
||||
self.assert500(resp)
|
||||
self.assertIn('Bad value', resp.json['data'])
|
||||
resp = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data=json.dumps({"contributionMatching": 2}))
|
||||
self.assert400(resp)
|
||||
self.assertTrue(resp.json['message'])
|
||||
|
||||
@patch('requests.get', side_effect=mock_blockchain_api_requests)
|
||||
def test_approve_proposal(self, mock_get):
|
||||
|
@ -272,7 +272,7 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
|||
# approve
|
||||
resp = self.app.put(
|
||||
"/api/v1/admin/proposals/{}/approve".format(self.proposal.id),
|
||||
data={"isApprove": True}
|
||||
data=json.dumps({"isApprove": True})
|
||||
)
|
||||
self.assert200(resp)
|
||||
self.assertEqual(resp.json["status"], ProposalStatus.APPROVED)
|
||||
|
@ -287,7 +287,7 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
|||
# reject
|
||||
resp = self.app.put(
|
||||
"/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.assertEqual(resp.json["status"], ProposalStatus.REJECTED)
|
||||
|
@ -301,10 +301,43 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
|||
# nominate arbiter
|
||||
resp = self.app.put(
|
||||
"/api/v1/admin/arbiters",
|
||||
data={
|
||||
data=json.dumps({
|
||||
'proposalId': self.proposal.id,
|
||||
'userId': self.other_user.id
|
||||
}
|
||||
})
|
||||
)
|
||||
self.assert200(resp)
|
||||
# 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)
|
||||
|
|
@ -45,7 +45,7 @@ class TestProposalAPI(BaseProposalCreatorConfig):
|
|||
data=json.dumps(new_proposal),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
print(resp)
|
||||
self.assert200(resp)
|
||||
self.assertEqual(resp.json["title"], new_title)
|
||||
self.assertEqual(self.proposal.title, new_title)
|
||||
|
|
|
@ -104,10 +104,10 @@ class TestUserAPI(BaseUserConfig):
|
|||
}),
|
||||
content_type="application/json"
|
||||
)
|
||||
# self.assert403(user_auth_resp)
|
||||
# self.assertTrue(user_auth_resp.json['message'] is not None)
|
||||
self.assert500(user_auth_resp)
|
||||
self.assertIn('Invalid pass', user_auth_resp.json['data'])
|
||||
self.assert403(user_auth_resp)
|
||||
self.assertTrue(user_auth_resp.json['message'] is not None)
|
||||
# self.assert500(user_auth_resp)
|
||||
# self.assertIn('Invalid pass', user_auth_resp.json['data'])
|
||||
|
||||
def test_user_auth_bad_email(self):
|
||||
user_auth_resp = self.app.post(
|
||||
|
@ -118,10 +118,10 @@ class TestUserAPI(BaseUserConfig):
|
|||
}),
|
||||
content_type="application/json"
|
||||
)
|
||||
# self.assert400(user_auth_resp)
|
||||
# self.assertTrue(user_auth_resp.json['message'] is not None)
|
||||
self.assert500(user_auth_resp)
|
||||
self.assertIn('No user', user_auth_resp.json['data'])
|
||||
self.assert403(user_auth_resp)
|
||||
self.assertTrue(user_auth_resp.json['message'] is not None)
|
||||
# self.assert500(user_auth_resp)
|
||||
# self.assertIn('No user', user_auth_resp.json['data'])
|
||||
|
||||
def test_user_auth_banned(self):
|
||||
self.user.set_banned(True, 'reason for banning')
|
||||
|
@ -134,8 +134,8 @@ class TestUserAPI(BaseUserConfig):
|
|||
content_type="application/json"
|
||||
)
|
||||
# in test mode we get 500s instead of 403
|
||||
self.assert500(user_auth_resp)
|
||||
self.assertIn('banned', user_auth_resp.json['data'])
|
||||
self.assert403(user_auth_resp)
|
||||
self.assertIn('banned', user_auth_resp.json['message'])
|
||||
|
||||
def test_create_user_duplicate_400(self):
|
||||
# self.user is identical to test_user, should throw
|
||||
|
@ -152,7 +152,7 @@ class TestUserAPI(BaseUserConfig):
|
|||
self.login_default_user()
|
||||
updated_user = animalify(copy.deepcopy(user_schema.dump(self.user)))
|
||||
updated_user["displayName"] = 'new display name'
|
||||
updated_user["avatar"] = {}
|
||||
updated_user["avatar"] = '' # TODO confirm avatar is no longer a dict
|
||||
updated_user["socialMedias"] = []
|
||||
|
||||
user_update_resp = self.app.put(
|
||||
|
@ -253,8 +253,9 @@ class TestUserAPI(BaseUserConfig):
|
|||
content_type='application/json'
|
||||
)
|
||||
# 404 outside testing mode
|
||||
self.assertStatus(response, 500)
|
||||
self.assertIn('banned', response.json['data'])
|
||||
self.assertStatus(response, 403)
|
||||
print(response.json)
|
||||
self.assertIn('banned', response.json['message'])
|
||||
|
||||
def test_recover_user_no_user(self):
|
||||
response = self.app.post(
|
||||
|
@ -301,8 +302,8 @@ class TestUserAPI(BaseUserConfig):
|
|||
content_type='application/json'
|
||||
)
|
||||
# 403 outside of testing mode
|
||||
self.assertStatus(reset_resp, 500)
|
||||
self.assertIn('banned', reset_resp.json['data'])
|
||||
self.assertStatus(reset_resp, 403)
|
||||
self.assertIn('banned', reset_resp.json['message'])
|
||||
|
||||
@patch('grant.user.views.verify_social')
|
||||
def test_user_verify_social(self, mock_verify_social):
|
||||
|
|
Loading…
Reference in New Issue