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 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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue