2019-02-13 08:54:46 -08:00
|
|
|
from functools import reduce
|
2019-01-16 21:20:10 -08:00
|
|
|
from flask import Blueprint, request
|
2018-10-30 09:35:47 -07:00
|
|
|
from flask_yoloapi import endpoint, parameter
|
2019-02-06 08:21:19 -08:00
|
|
|
from decimal import Decimal
|
2019-02-19 09:20:17 -08:00
|
|
|
from datetime import datetime
|
2019-02-17 08:52:35 -08:00
|
|
|
from sqlalchemy import text
|
|
|
|
|
2019-01-23 07:00:30 -08:00
|
|
|
from grant.comment.models import Comment, user_comments_schema
|
2019-02-06 12:56:21 -08:00
|
|
|
from grant.email.send import generate_email, send_email
|
2018-10-30 09:35:47 -07:00
|
|
|
from grant.extensions import db
|
2019-01-16 21:01:29 -08:00
|
|
|
from grant.proposal.models import (
|
|
|
|
Proposal,
|
2019-02-09 18:58:40 -08:00
|
|
|
ProposalArbiter,
|
2019-01-16 21:01:29 -08:00
|
|
|
ProposalContribution,
|
|
|
|
proposals_schema,
|
|
|
|
proposal_schema,
|
|
|
|
user_proposal_contributions_schema,
|
2019-02-17 08:52:35 -08:00
|
|
|
admin_proposal_contribution_schema,
|
2019-02-19 09:13:13 -08:00
|
|
|
admin_proposal_contributions_schema,
|
2019-01-16 21:01:29 -08:00
|
|
|
)
|
2019-02-13 08:54:46 -08:00
|
|
|
from grant.milestone.models import Milestone
|
2019-02-17 08:52:35 -08:00
|
|
|
from grant.user.models import User, UserSettings, admin_users_schema, admin_user_schema
|
2019-02-01 11:13:30 -08:00
|
|
|
from grant.rfp.models import RFP, admin_rfp_schema, admin_rfps_schema
|
2019-01-23 07:00:30 -08:00
|
|
|
from grant.utils.admin import admin_auth_required, admin_is_authed, admin_login, admin_logout
|
2019-02-06 12:56:21 -08:00
|
|
|
from grant.utils.misc import make_url
|
2019-02-14 16:08:18 -08:00
|
|
|
from grant.utils.enums import (
|
|
|
|
ProposalStatus,
|
|
|
|
ProposalStage,
|
|
|
|
ContributionStatus,
|
|
|
|
ProposalArbiterStatus,
|
|
|
|
MilestoneStage,
|
|
|
|
RFPStatus,
|
|
|
|
)
|
2019-02-05 12:34:19 -08:00
|
|
|
from grant.utils import pagination
|
2019-02-13 12:30:58 -08:00
|
|
|
from grant.settings import EXPLORER_URL
|
2019-01-23 07:00:30 -08:00
|
|
|
from sqlalchemy import func, or_
|
2018-10-30 09:35:47 -07:00
|
|
|
|
2019-01-23 07:00:30 -08:00
|
|
|
from .example_emails import example_email_args
|
2018-10-30 09:35:47 -07:00
|
|
|
|
|
|
|
blueprint = Blueprint('admin', __name__, url_prefix='/api/v1/admin')
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/checklogin", methods=["GET"])
|
|
|
|
@endpoint.api()
|
|
|
|
def loggedin():
|
2019-01-16 21:20:10 -08:00
|
|
|
return {"isLoggedIn": admin_is_authed()}
|
2018-10-30 09:35:47 -07:00
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/login", methods=["POST"])
|
|
|
|
@endpoint.api(
|
|
|
|
parameter('username', type=str, required=False),
|
|
|
|
parameter('password', type=str, required=False),
|
|
|
|
)
|
|
|
|
def login(username, password):
|
2019-01-16 21:20:10 -08:00
|
|
|
if admin_login(username, password):
|
2018-10-30 09:35:47 -07:00
|
|
|
return {"isLoggedIn": True}
|
|
|
|
else:
|
|
|
|
return {"message": "Username or password incorrect."}, 401
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/logout", methods=["GET"])
|
|
|
|
@endpoint.api()
|
|
|
|
def logout():
|
2019-01-16 21:20:10 -08:00
|
|
|
admin_logout()
|
2018-10-30 09:35:47 -07:00
|
|
|
return {"isLoggedIn": False}
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/stats", methods=["GET"])
|
|
|
|
@endpoint.api()
|
2019-01-16 21:20:10 -08:00
|
|
|
@admin_auth_required
|
2018-10-30 09:35:47 -07:00
|
|
|
def stats():
|
|
|
|
user_count = db.session.query(func.count(User.id)).scalar()
|
|
|
|
proposal_count = db.session.query(func.count(Proposal.id)).scalar()
|
2019-01-09 10:23:08 -08:00
|
|
|
proposal_pending_count = db.session.query(func.count(Proposal.id)) \
|
2019-01-30 09:59:15 -08:00
|
|
|
.filter(Proposal.status == ProposalStatus.PENDING) \
|
2019-01-09 10:23:08 -08:00
|
|
|
.scalar()
|
2019-02-05 12:45:26 -08:00
|
|
|
proposal_no_arbiter_count = db.session.query(func.count(Proposal.id)) \
|
2019-02-11 13:59:29 -08:00
|
|
|
.join(Proposal.arbiter) \
|
2019-02-05 12:45:26 -08:00
|
|
|
.filter(Proposal.status == ProposalStatus.LIVE) \
|
2019-02-09 18:58:40 -08:00
|
|
|
.filter(ProposalArbiter.status == ProposalArbiterStatus.MISSING) \
|
2019-02-05 12:45:26 -08:00
|
|
|
.scalar()
|
2019-02-13 08:54:46 -08:00
|
|
|
proposal_milestone_payouts_count = db.session.query(func.count(Proposal.id)) \
|
|
|
|
.join(Proposal.milestones) \
|
|
|
|
.filter(Proposal.status == ProposalStatus.LIVE) \
|
|
|
|
.filter(Milestone.stage == MilestoneStage.ACCEPTED) \
|
|
|
|
.scalar()
|
2019-02-17 08:52:35 -08:00
|
|
|
# Count contributions on proposals that didn't get funded for users who have specified a refund address
|
|
|
|
contribution_refundable_count = db.session.query(func.count(ProposalContribution.id)) \
|
|
|
|
.join(Proposal) \
|
2019-02-19 09:19:47 -08:00
|
|
|
.filter(Proposal.stage == ProposalStage.REFUNDING) \
|
2019-02-17 08:52:35 -08:00
|
|
|
.join(ProposalContribution.user) \
|
|
|
|
.join(UserSettings) \
|
|
|
|
.filter(UserSettings.refund_address != None) \
|
|
|
|
.scalar()
|
2018-10-30 09:35:47 -07:00
|
|
|
return {
|
|
|
|
"userCount": user_count,
|
2019-01-09 10:23:08 -08:00
|
|
|
"proposalCount": proposal_count,
|
|
|
|
"proposalPendingCount": proposal_pending_count,
|
2019-02-05 12:45:26 -08:00
|
|
|
"proposalNoArbiterCount": proposal_no_arbiter_count,
|
2019-02-13 08:54:46 -08:00
|
|
|
"proposalMilestonePayoutsCount": proposal_milestone_payouts_count,
|
2019-02-17 08:52:35 -08:00
|
|
|
"contributionRefundableCount": contribution_refundable_count,
|
2018-10-30 09:35:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-01-16 21:01:29 -08:00
|
|
|
# USERS
|
|
|
|
|
|
|
|
|
2019-02-04 13:18:50 -08:00
|
|
|
@blueprint.route('/users/<user_id>', methods=['DELETE'])
|
2018-10-30 09:35:47 -07:00
|
|
|
@endpoint.api()
|
2019-01-16 21:20:10 -08:00
|
|
|
@admin_auth_required
|
2019-02-04 13:18:50 -08:00
|
|
|
def delete_user(user_id):
|
|
|
|
user = User.query.filter(User.id == user_id).first()
|
|
|
|
if not user:
|
|
|
|
return {"message": "No user matching that id"}, 404
|
|
|
|
|
|
|
|
db.session.delete(user)
|
|
|
|
db.session.commit()
|
|
|
|
return None, 200
|
2018-10-30 09:35:47 -07:00
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/users", methods=["GET"])
|
2019-02-14 20:11:47 -08:00
|
|
|
@endpoint.api(
|
|
|
|
parameter('page', type=int, required=False),
|
|
|
|
parameter('filters', type=list, required=False),
|
|
|
|
parameter('search', type=str, required=False),
|
|
|
|
parameter('sort', type=str, required=False)
|
|
|
|
)
|
2019-01-16 21:20:10 -08:00
|
|
|
@admin_auth_required
|
2019-02-14 20:11:47 -08:00
|
|
|
def get_users(page, filters, search, sort):
|
|
|
|
filters_workaround = request.args.getlist('filters[]')
|
|
|
|
page = pagination.user(
|
|
|
|
schema=admin_users_schema,
|
|
|
|
query=User.query,
|
|
|
|
page=page,
|
|
|
|
filters=filters_workaround,
|
|
|
|
search=search,
|
|
|
|
sort=sort,
|
|
|
|
)
|
|
|
|
return page
|
2019-01-16 21:01:29 -08:00
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/users/<id>', methods=['GET'])
|
|
|
|
@endpoint.api()
|
2019-01-16 21:20:10 -08:00
|
|
|
@admin_auth_required
|
2019-01-16 21:01:29 -08:00
|
|
|
def get_user(id):
|
|
|
|
user_db = User.query.filter(User.id == id).first()
|
|
|
|
if user_db:
|
2019-02-06 06:31:38 -08:00
|
|
|
user = admin_user_schema.dump(user_db)
|
2018-10-30 09:35:47 -07:00
|
|
|
user_proposals = Proposal.query.filter(Proposal.team.any(id=user['userid'])).all()
|
|
|
|
user['proposals'] = proposals_schema.dump(user_proposals)
|
2019-01-16 21:01:29 -08:00
|
|
|
user_comments = Comment.get_by_user(user_db)
|
|
|
|
user['comments'] = user_comments_schema.dump(user_comments)
|
|
|
|
contributions = ProposalContribution.get_by_userid(user_db.id)
|
|
|
|
contributions_dump = user_proposal_contributions_schema.dump(contributions)
|
|
|
|
user["contributions"] = contributions_dump
|
|
|
|
return user
|
|
|
|
return {"message": f"Could not find user with id {id}"}, 404
|
|
|
|
|
|
|
|
|
2019-02-14 20:11:47 -08:00
|
|
|
@blueprint.route('/users/<user_id>', methods=['PUT'])
|
|
|
|
@endpoint.api(
|
|
|
|
parameter('silenced', type=bool, required=False),
|
|
|
|
parameter('banned', type=bool, required=False),
|
|
|
|
parameter('bannedReason', type=str, required=False),
|
|
|
|
)
|
|
|
|
@admin_auth_required
|
|
|
|
def edit_user(user_id, silenced, banned, banned_reason):
|
|
|
|
user = User.query.filter(User.id == user_id).first()
|
|
|
|
if not user:
|
|
|
|
return {"message": f"Could not find user with id {id}"}, 404
|
|
|
|
|
|
|
|
if silenced is not None:
|
|
|
|
user.silenced = silenced
|
|
|
|
db.session.add(user)
|
|
|
|
|
|
|
|
if banned is not None:
|
|
|
|
if banned and not banned_reason: # if banned true, provide reason
|
|
|
|
return {"message": "Please include reason for banning"}, 417
|
|
|
|
user.banned = banned
|
|
|
|
user.banned_reason = banned_reason
|
|
|
|
db.session.add(user)
|
|
|
|
|
|
|
|
db.session.commit()
|
|
|
|
return admin_user_schema.dump(user)
|
|
|
|
|
|
|
|
|
2019-02-06 10:31:53 -08:00
|
|
|
# ARBITERS
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/arbiters", methods=["GET"])
|
|
|
|
@endpoint.api(
|
|
|
|
parameter('search', type=str, required=False),
|
|
|
|
)
|
|
|
|
@admin_auth_required
|
|
|
|
def get_arbiters(search):
|
|
|
|
results = []
|
|
|
|
error = None
|
|
|
|
if len(search) < 3:
|
|
|
|
error = 'search query must be at least 3 characters long'
|
|
|
|
else:
|
|
|
|
users = User.query.filter(
|
|
|
|
User.email_address.ilike(f'%{search}%') | User.display_name.ilike(f'%{search}%')
|
2019-02-06 10:56:08 -08:00
|
|
|
).order_by(User.display_name).all()
|
2019-02-06 10:31:53 -08:00
|
|
|
results = admin_users_schema.dump(users)
|
|
|
|
|
|
|
|
return {
|
|
|
|
'results': results,
|
|
|
|
'search': search,
|
|
|
|
'error': error
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/arbiters', methods=['PUT'])
|
|
|
|
@endpoint.api(
|
|
|
|
parameter('proposalId', type=int, required=True),
|
|
|
|
parameter('userId', type=int, required=True)
|
|
|
|
)
|
|
|
|
@admin_auth_required
|
|
|
|
def set_arbiter(proposal_id, user_id):
|
|
|
|
proposal = Proposal.query.filter(Proposal.id == proposal_id).first()
|
|
|
|
if not proposal:
|
|
|
|
return {"message": "Proposal not found"}, 404
|
|
|
|
|
2019-02-15 19:35:25 -08:00
|
|
|
for member in proposal.team:
|
|
|
|
if member.id == user_id:
|
|
|
|
return {"message": "Cannot set proposal team member as arbiter"}, 400
|
|
|
|
|
|
|
|
if proposal.is_failed:
|
|
|
|
return {"message": "Cannot set arbiter on failed proposal"}, 400
|
|
|
|
|
2019-02-06 10:31:53 -08:00
|
|
|
user = User.query.filter(User.id == user_id).first()
|
|
|
|
if not user:
|
|
|
|
return {"message": "User not found"}, 404
|
|
|
|
|
2019-02-09 18:58:40 -08:00
|
|
|
# send email
|
|
|
|
code = user.email_verification.code
|
|
|
|
send_email(user.email_address, 'proposal_arbiter', {
|
|
|
|
'proposal': proposal,
|
|
|
|
'proposal_url': make_url(f'/proposals/{proposal.id}'),
|
|
|
|
'accept_url': make_url(f'/email/arbiter?code={code}&proposalId={proposal.id}'),
|
|
|
|
})
|
|
|
|
proposal.arbiter.user = user
|
|
|
|
proposal.arbiter.status = ProposalArbiterStatus.NOMINATED
|
|
|
|
db.session.add(proposal.arbiter)
|
|
|
|
db.session.commit()
|
2019-02-06 12:56:21 -08:00
|
|
|
|
2019-02-06 10:31:53 -08:00
|
|
|
return {
|
|
|
|
'proposal': proposal_schema.dump(proposal),
|
|
|
|
'user': admin_user_schema.dump(user)
|
|
|
|
}, 200
|
|
|
|
|
|
|
|
|
2019-01-16 21:01:29 -08:00
|
|
|
# PROPOSALS
|
2018-10-30 09:35:47 -07:00
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/proposals", methods=["GET"])
|
2019-02-05 12:34:19 -08:00
|
|
|
@endpoint.api(
|
|
|
|
parameter('page', type=int, required=False),
|
|
|
|
parameter('filters', type=list, required=False),
|
|
|
|
parameter('search', type=str, required=False),
|
|
|
|
parameter('sort', type=str, required=False)
|
|
|
|
)
|
2019-01-16 21:20:10 -08:00
|
|
|
@admin_auth_required
|
2019-02-05 12:34:19 -08:00
|
|
|
def get_proposals(page, filters, search, sort):
|
|
|
|
filters_workaround = request.args.getlist('filters[]')
|
|
|
|
page = pagination.proposal(
|
|
|
|
schema=proposals_schema,
|
|
|
|
query=Proposal.query,
|
|
|
|
page=page,
|
|
|
|
filters=filters_workaround,
|
|
|
|
search=search,
|
|
|
|
sort=sort,
|
|
|
|
)
|
|
|
|
return page
|
2018-10-30 09:35:47 -07:00
|
|
|
|
|
|
|
|
2019-01-09 10:23:08 -08:00
|
|
|
@blueprint.route('/proposals/<id>', methods=['GET'])
|
|
|
|
@endpoint.api()
|
2019-01-16 21:20:10 -08:00
|
|
|
@admin_auth_required
|
2019-01-09 10:23:08 -08:00
|
|
|
def get_proposal(id):
|
|
|
|
proposal = Proposal.query.filter(Proposal.id == id).first()
|
|
|
|
if proposal:
|
|
|
|
return proposal_schema.dump(proposal)
|
2019-01-29 15:50:27 -08:00
|
|
|
return {"message": f"Could not find proposal with id {id}"}, 404
|
2019-01-09 10:23:08 -08:00
|
|
|
|
|
|
|
|
2018-10-30 09:35:47 -07:00
|
|
|
@blueprint.route('/proposals/<id>', methods=['DELETE'])
|
|
|
|
@endpoint.api()
|
2019-01-16 21:20:10 -08:00
|
|
|
@admin_auth_required
|
2018-10-30 09:35:47 -07:00
|
|
|
def delete_proposal(id):
|
2018-11-01 18:35:14 -07:00
|
|
|
return {"message": "Not implemented."}, 400
|
2019-01-09 10:23:08 -08:00
|
|
|
|
|
|
|
|
2019-01-29 15:50:27 -08:00
|
|
|
@blueprint.route('/proposals/<id>', methods=['PUT'])
|
|
|
|
@endpoint.api(
|
|
|
|
parameter('contributionMatching', type=float, required=False, default=None)
|
|
|
|
)
|
|
|
|
@admin_auth_required
|
|
|
|
def update_proposal(id, contribution_matching):
|
|
|
|
proposal = Proposal.query.filter(Proposal.id == id).first()
|
|
|
|
if proposal:
|
|
|
|
if contribution_matching is not None:
|
2019-02-15 19:35:25 -08:00
|
|
|
proposal.set_contribution_matching(contribution_matching)
|
2019-01-29 15:50:27 -08:00
|
|
|
|
|
|
|
db.session.commit()
|
|
|
|
return proposal_schema.dump(proposal)
|
|
|
|
|
|
|
|
return {"message": f"Could not find proposal with id {id}"}, 404
|
|
|
|
|
|
|
|
|
2019-01-09 10:23:08 -08:00
|
|
|
@blueprint.route('/proposals/<id>/approve', methods=['PUT'])
|
|
|
|
@endpoint.api(
|
|
|
|
parameter('isApprove', type=bool, required=True),
|
|
|
|
parameter('rejectReason', type=str, required=False)
|
|
|
|
)
|
2019-01-16 21:20:10 -08:00
|
|
|
@admin_auth_required
|
2019-01-09 10:23:08 -08:00
|
|
|
def approve_proposal(id, is_approve, reject_reason=None):
|
|
|
|
proposal = Proposal.query.filter_by(id=id).first()
|
|
|
|
if proposal:
|
|
|
|
proposal.approve_pending(is_approve, reject_reason)
|
|
|
|
db.session.commit()
|
|
|
|
return proposal_schema.dump(proposal)
|
|
|
|
|
2019-02-13 08:54:46 -08:00
|
|
|
return {"message": "No proposal found."}, 404
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/proposals/<id>/milestone/<mid>/paid", methods=["PUT"])
|
|
|
|
@endpoint.api(
|
|
|
|
parameter('txId', type=str, required=True),
|
|
|
|
)
|
|
|
|
@admin_auth_required
|
|
|
|
def paid_milestone_payout_request(id, mid, tx_id):
|
|
|
|
proposal = Proposal.query.filter_by(id=id).first()
|
|
|
|
if not proposal:
|
|
|
|
return {"message": "No proposal matching id"}, 404
|
|
|
|
if not proposal.is_funded:
|
|
|
|
return {"message": "Proposal is not fully funded"}, 400
|
|
|
|
for ms in proposal.milestones:
|
|
|
|
if ms.id == int(mid):
|
|
|
|
ms.mark_paid(tx_id)
|
|
|
|
db.session.add(ms)
|
|
|
|
db.session.flush()
|
2019-02-13 12:30:58 -08:00
|
|
|
# check if this is the final ms, and update proposal.stage
|
2019-02-13 08:54:46 -08:00
|
|
|
num_paid = reduce(lambda a, x: a + (1 if x.stage == MilestoneStage.PAID else 0), proposal.milestones, 0)
|
|
|
|
if num_paid == len(proposal.milestones):
|
|
|
|
proposal.stage = ProposalStage.COMPLETED # WIP -> COMPLETED
|
|
|
|
db.session.add(proposal)
|
|
|
|
db.session.flush()
|
|
|
|
db.session.commit()
|
2019-02-13 12:30:58 -08:00
|
|
|
# email TEAM that payout request was PAID
|
|
|
|
amount = Decimal(ms.payout_percent) * Decimal(proposal.target) / 100
|
|
|
|
for member in proposal.team:
|
|
|
|
send_email(member.email_address, 'milestone_paid', {
|
|
|
|
'proposal': proposal,
|
|
|
|
'amount': amount,
|
|
|
|
'tx_explorer_url': f'{EXPLORER_URL}transactions/{tx_id}',
|
|
|
|
'proposal_milestones_url': make_url(f'/proposals/{proposal.id}?tab=milestones'),
|
|
|
|
})
|
2019-02-13 08:54:46 -08:00
|
|
|
return proposal_schema.dump(proposal), 200
|
|
|
|
|
|
|
|
return {"message": "No milestone matching id"}, 404
|
2019-01-09 11:08:25 -08:00
|
|
|
|
|
|
|
|
2019-01-16 21:01:29 -08:00
|
|
|
# EMAIL
|
|
|
|
|
|
|
|
|
2019-01-09 11:08:25 -08:00
|
|
|
@blueprint.route('/email/example/<type>', methods=['GET'])
|
|
|
|
@endpoint.api()
|
2019-01-16 21:20:10 -08:00
|
|
|
@admin_auth_required
|
2019-01-09 11:08:25 -08:00
|
|
|
def get_email_example(type):
|
2019-01-16 14:26:45 -08:00
|
|
|
email = generate_email(type, example_email_args.get(type))
|
|
|
|
if email['info'].get('subscription'):
|
|
|
|
# Unserializable, so remove
|
|
|
|
email['info'].pop('subscription', None)
|
|
|
|
return email
|
2019-01-30 09:59:15 -08:00
|
|
|
|
|
|
|
|
|
|
|
# Requests for Proposal
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/rfps', methods=['GET'])
|
|
|
|
@endpoint.api()
|
|
|
|
@admin_auth_required
|
|
|
|
def get_rfps():
|
|
|
|
rfps = RFP.query.all()
|
2019-02-01 11:13:30 -08:00
|
|
|
return admin_rfps_schema.dump(rfps)
|
2019-01-30 09:59:15 -08:00
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/rfps', methods=['POST'])
|
|
|
|
@endpoint.api(
|
|
|
|
parameter('title', type=str),
|
|
|
|
parameter('brief', type=str),
|
|
|
|
parameter('content', type=str),
|
|
|
|
parameter('category', type=str),
|
2019-02-07 12:35:33 -08:00
|
|
|
parameter('bounty', type=str),
|
2019-02-08 11:02:34 -08:00
|
|
|
parameter('matching', type=bool, default=False),
|
2019-02-07 12:35:33 -08:00
|
|
|
parameter('dateCloses', type=int),
|
2019-01-30 09:59:15 -08:00
|
|
|
)
|
|
|
|
@admin_auth_required
|
2019-02-08 11:02:34 -08:00
|
|
|
def create_rfp(date_closes, **kwargs):
|
2019-01-30 09:59:15 -08:00
|
|
|
rfp = RFP(
|
2019-02-08 11:02:34 -08:00
|
|
|
**kwargs,
|
|
|
|
date_closes=datetime.fromtimestamp(date_closes) if date_closes else None,
|
2019-01-30 09:59:15 -08:00
|
|
|
)
|
|
|
|
db.session.add(rfp)
|
|
|
|
db.session.commit()
|
2019-02-01 11:13:30 -08:00
|
|
|
return admin_rfp_schema.dump(rfp), 201
|
2019-01-30 09:59:15 -08:00
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/rfps/<rfp_id>', methods=['GET'])
|
|
|
|
@endpoint.api()
|
|
|
|
@admin_auth_required
|
|
|
|
def get_rfp(rfp_id):
|
|
|
|
rfp = RFP.query.filter(RFP.id == rfp_id).first()
|
|
|
|
if not rfp:
|
|
|
|
return {"message": "No RFP matching that id"}, 404
|
|
|
|
|
2019-02-01 11:13:30 -08:00
|
|
|
return admin_rfp_schema.dump(rfp)
|
2019-01-30 09:59:15 -08:00
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/rfps/<rfp_id>', methods=['PUT'])
|
|
|
|
@endpoint.api(
|
|
|
|
parameter('title', type=str),
|
|
|
|
parameter('brief', type=str),
|
|
|
|
parameter('content', type=str),
|
|
|
|
parameter('category', type=str),
|
2019-02-07 12:35:33 -08:00
|
|
|
parameter('bounty', type=str),
|
2019-02-08 08:54:20 -08:00
|
|
|
parameter('matching', type=bool, default=False),
|
2019-02-07 12:35:33 -08:00
|
|
|
parameter('dateCloses', type=int),
|
2019-01-30 09:59:15 -08:00
|
|
|
parameter('status', type=str),
|
|
|
|
)
|
|
|
|
@admin_auth_required
|
2019-02-07 12:35:33 -08:00
|
|
|
def update_rfp(rfp_id, title, brief, content, category, bounty, matching, date_closes, status):
|
2019-01-30 09:59:15 -08:00
|
|
|
rfp = RFP.query.filter(RFP.id == rfp_id).first()
|
|
|
|
if not rfp:
|
|
|
|
return {"message": "No RFP matching that id"}, 404
|
|
|
|
|
2019-02-07 12:35:33 -08:00
|
|
|
# Update fields
|
2019-01-30 09:59:15 -08:00
|
|
|
rfp.title = title
|
|
|
|
rfp.brief = brief
|
|
|
|
rfp.content = content
|
|
|
|
rfp.category = category
|
2019-02-07 12:35:33 -08:00
|
|
|
rfp.bounty = bounty
|
|
|
|
rfp.matching = matching
|
2019-02-08 08:54:20 -08:00
|
|
|
rfp.date_closes = datetime.fromtimestamp(date_closes) if date_closes else None
|
2019-02-07 12:35:33 -08:00
|
|
|
|
|
|
|
# Update timestamps if status changed
|
|
|
|
if rfp.status != status:
|
2019-02-08 11:02:34 -08:00
|
|
|
if status == RFPStatus.LIVE and not rfp.date_opened:
|
2019-02-07 12:35:33 -08:00
|
|
|
rfp.date_opened = datetime.now()
|
|
|
|
if status == RFPStatus.CLOSED:
|
|
|
|
rfp.date_closed = datetime.now()
|
|
|
|
rfp.status = status
|
2019-01-30 09:59:15 -08:00
|
|
|
|
|
|
|
db.session.add(rfp)
|
|
|
|
db.session.commit()
|
2019-02-01 11:13:30 -08:00
|
|
|
return admin_rfp_schema.dump(rfp)
|
2019-01-30 09:59:15 -08:00
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/rfps/<rfp_id>', methods=['DELETE'])
|
|
|
|
@endpoint.api()
|
|
|
|
@admin_auth_required
|
|
|
|
def delete_rfp(rfp_id):
|
|
|
|
rfp = RFP.query.filter(RFP.id == rfp_id).first()
|
|
|
|
if not rfp:
|
|
|
|
return {"message": "No RFP matching that id"}, 404
|
|
|
|
|
|
|
|
db.session.delete(rfp)
|
|
|
|
db.session.commit()
|
|
|
|
return None, 200
|
2019-02-06 08:21:19 -08:00
|
|
|
|
|
|
|
|
|
|
|
# Contributions
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/contributions', methods=['GET'])
|
|
|
|
@endpoint.api(
|
|
|
|
parameter('page', type=int, required=False),
|
|
|
|
parameter('filters', type=list, required=False),
|
|
|
|
parameter('search', type=str, required=False),
|
|
|
|
parameter('sort', type=str, required=False)
|
|
|
|
)
|
|
|
|
@admin_auth_required
|
|
|
|
def get_contributions(page, filters, search, sort):
|
|
|
|
filters_workaround = request.args.getlist('filters[]')
|
|
|
|
page = pagination.contribution(
|
|
|
|
page=page,
|
2019-02-19 09:13:13 -08:00
|
|
|
schema=admin_proposal_contributions_schema,
|
2019-02-06 08:21:19 -08:00
|
|
|
filters=filters_workaround,
|
|
|
|
search=search,
|
|
|
|
sort=sort,
|
|
|
|
)
|
|
|
|
return page
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/contributions', methods=['POST'])
|
|
|
|
@endpoint.api(
|
2019-02-06 11:01:46 -08:00
|
|
|
parameter('proposalId', type=int, required=True),
|
|
|
|
parameter('userId', type=int, required=True),
|
2019-02-06 08:21:19 -08:00
|
|
|
parameter('status', type=str, required=True),
|
|
|
|
parameter('amount', type=str, required=True),
|
2019-02-06 11:01:46 -08:00
|
|
|
parameter('txId', type=str, required=False),
|
2019-02-06 08:21:19 -08:00
|
|
|
)
|
|
|
|
@admin_auth_required
|
|
|
|
def create_contribution(proposal_id, user_id, status, amount, tx_id):
|
2019-02-06 11:01:46 -08:00
|
|
|
# Some fields set manually since we're admin, and normally don't do this
|
2019-02-06 08:21:19 -08:00
|
|
|
contribution = ProposalContribution(
|
|
|
|
proposal_id=proposal_id,
|
|
|
|
user_id=user_id,
|
|
|
|
amount=amount,
|
|
|
|
)
|
2019-02-06 11:01:46 -08:00
|
|
|
contribution.status = status
|
|
|
|
contribution.tx_id = tx_id
|
|
|
|
|
2019-02-06 08:21:19 -08:00
|
|
|
db.session.add(contribution)
|
2019-02-15 19:35:25 -08:00
|
|
|
db.session.flush()
|
|
|
|
|
|
|
|
contribution.proposal.set_pending_when_ready()
|
|
|
|
contribution.proposal.set_funded_when_ready()
|
|
|
|
|
2019-02-06 08:21:19 -08:00
|
|
|
db.session.commit()
|
2019-02-17 08:52:35 -08:00
|
|
|
return admin_proposal_contribution_schema.dump(contribution), 200
|
2019-02-06 08:21:19 -08:00
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/contributions/<contribution_id>', methods=['GET'])
|
|
|
|
@endpoint.api()
|
|
|
|
@admin_auth_required
|
|
|
|
def get_contribution(contribution_id):
|
|
|
|
contribution = ProposalContribution.query.filter(ProposalContribution.id == contribution_id).first()
|
|
|
|
if not contribution:
|
|
|
|
return {"message": "No contribution matching that id"}, 404
|
|
|
|
|
2019-02-17 08:52:35 -08:00
|
|
|
return admin_proposal_contribution_schema.dump(contribution), 200
|
2019-02-06 08:21:19 -08:00
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route('/contributions/<contribution_id>', methods=['PUT'])
|
|
|
|
@endpoint.api(
|
2019-02-06 11:01:46 -08:00
|
|
|
parameter('proposalId', type=int, required=False),
|
|
|
|
parameter('userId', type=int, required=False),
|
2019-02-06 08:21:19 -08:00
|
|
|
parameter('status', type=str, required=False),
|
|
|
|
parameter('amount', type=str, required=False),
|
2019-02-06 11:01:46 -08:00
|
|
|
parameter('txId', type=str, required=False),
|
2019-02-17 08:52:35 -08:00
|
|
|
parameter('refundTxId', type=str, required=False),
|
2019-02-06 08:21:19 -08:00
|
|
|
)
|
|
|
|
@admin_auth_required
|
2019-02-17 08:52:35 -08:00
|
|
|
def edit_contribution(contribution_id, proposal_id, user_id, status, amount, tx_id, refund_tx_id):
|
2019-02-06 08:21:19 -08:00
|
|
|
contribution = ProposalContribution.query.filter(ProposalContribution.id == contribution_id).first()
|
|
|
|
if not contribution:
|
|
|
|
return {"message": "No contribution matching that id"}, 404
|
2019-02-19 09:13:13 -08:00
|
|
|
had_refund = contribution.refund_tx_id
|
2019-02-06 08:21:19 -08:00
|
|
|
|
2019-02-15 19:35:25 -08:00
|
|
|
# do not allow editing contributions once a proposal has become funded
|
|
|
|
if contribution.proposal.is_funded:
|
|
|
|
return {"message": "Cannot edit contributions to fully-funded proposals"}, 400
|
|
|
|
|
2019-02-06 08:21:19 -08:00
|
|
|
# Proposal ID (must belong to an existing proposal)
|
|
|
|
if proposal_id:
|
|
|
|
proposal = Proposal.query.filter(Proposal.id == proposal_id).first()
|
|
|
|
if not proposal:
|
|
|
|
return {"message": "No proposal matching that id"}, 400
|
|
|
|
contribution.proposal_id = proposal_id
|
|
|
|
# User ID (must belong to an existing user)
|
|
|
|
if user_id:
|
|
|
|
user = User.query.filter(User.id == user_id).first()
|
|
|
|
if not user:
|
|
|
|
return {"message": "No user matching that id"}, 400
|
|
|
|
contribution.user_id = user_id
|
|
|
|
# Status (must be in list of statuses)
|
|
|
|
if status:
|
|
|
|
if not ContributionStatus.includes(status):
|
|
|
|
return {"message": "Invalid status"}, 400
|
|
|
|
contribution.status = status
|
2019-02-19 09:13:13 -08:00
|
|
|
# Amount (must be a Decimal parseable)
|
2019-02-06 08:21:19 -08:00
|
|
|
if amount:
|
|
|
|
try:
|
2019-02-06 14:24:07 -08:00
|
|
|
contribution.amount = str(Decimal(amount))
|
2019-02-06 08:21:19 -08:00
|
|
|
except:
|
|
|
|
return {"message": "Amount could not be parsed as number"}, 400
|
|
|
|
# Transaction ID (no validation)
|
|
|
|
if tx_id:
|
|
|
|
contribution.tx_id = tx_id
|
2019-02-17 08:52:35 -08:00
|
|
|
# Refund TX ID (no validation)
|
|
|
|
if refund_tx_id:
|
|
|
|
contribution.refund_tx_id = refund_tx_id
|
2019-02-07 07:57:56 -08:00
|
|
|
|
2019-02-06 08:21:19 -08:00
|
|
|
db.session.add(contribution)
|
2019-02-15 19:35:25 -08:00
|
|
|
db.session.flush()
|
|
|
|
|
|
|
|
contribution.proposal.set_pending_when_ready()
|
|
|
|
contribution.proposal.set_funded_when_ready()
|
|
|
|
|
2019-02-06 08:21:19 -08:00
|
|
|
db.session.commit()
|
2019-02-19 09:13:13 -08:00
|
|
|
|
|
|
|
# Send email on refund txid
|
|
|
|
if not had_refund and contribution.refund_tx_id:
|
|
|
|
send_email(contribution.user.email_address, 'contribution_refunded', {
|
|
|
|
'contribution': contribution,
|
|
|
|
'proposal': contribution.proposal,
|
|
|
|
'tx_explorer_url': f'{EXPLORER_URL}transactions/{contribution.refund_tx_id}',
|
|
|
|
})
|
|
|
|
|
2019-02-17 08:52:35 -08:00
|
|
|
return admin_proposal_contribution_schema.dump(contribution), 200
|