Update Accepted Without Funding (#35)

* backend - init endpoints and model changes

* backend - add tests

* admin - add change to accepted with funding functionality

* backend - fix tests
This commit is contained in:
Danny Skubak 2019-10-23 17:44:19 -04:00 committed by Daniel Ternyak
parent 85c21d4cbf
commit 25e43a34ff
5 changed files with 154 additions and 8 deletions

View File

@ -36,6 +36,7 @@ type Props = RouteComponentProps<any>;
const STATE = {
paidTxId: '',
showCancelAndRefundPopover: false,
showChangeToAcceptedWithFundingPopover: false,
};
type State = typeof STATE;
@ -63,11 +64,12 @@ class ProposalDetailNaked extends React.Component<Props, State> {
return m.datePaid ? prev - parseFloat(m.payoutPercent) : prev;
}, 100);
const { isVersionTwo } = p
const { isVersionTwo } = p;
const shouldShowArbiter =
!isVersionTwo ||
(isVersionTwo && p.acceptedWithFunding === true);
const cancelButtonText = isVersionTwo ? 'Cancel' : 'Cancel & refund'
!isVersionTwo || (isVersionTwo && p.acceptedWithFunding === true);
const cancelButtonText = isVersionTwo ? 'Cancel' : 'Cancel & refund';
const shouldShowChangeToAcceptedWithFunding =
isVersionTwo && p.acceptedWithFunding === false;
const renderCancelControl = () => {
const disabled = this.getCancelAndRefundDisabled();
@ -99,7 +101,40 @@ class ProposalDetailNaked extends React.Component<Props, State> {
disabled={disabled}
block
>
{ cancelButtonText }
{cancelButtonText}
</Button>
</Popconfirm>
);
};
const renderChangeToAcceptedWithFundingControl = () => {
return (
<Popconfirm
title={
<p>
Are you sure you want to accept the proposal
<br />
with funding? This cannot be undone.
</p>
}
placement="left"
cancelText="cancel"
okText="confirm"
visible={this.state.showChangeToAcceptedWithFundingPopover}
okButtonProps={{
loading: store.proposalDetailCanceling,
}}
onCancel={this.handleChangeToAcceptWithFundingCancel}
onConfirm={this.handleChangeToAcceptWithFundingConfirm}
>
<Button
icon="close-circle"
className="ProposalDetail-controls-control"
loading={store.proposalDetailChangingToAcceptedWithFunding}
onClick={this.handleChangeToAcceptedWithFunding}
block
>
Accept With Funding
</Button>
</Popconfirm>
);
@ -120,7 +155,6 @@ class ProposalDetailNaked extends React.Component<Props, State> {
/>
);
const renderApproved = () =>
p.status === PROPOSAL_STATUS.APPROVED && (
<Alert
@ -203,7 +237,8 @@ class ProposalDetailNaked extends React.Component<Props, State> {
);
const renderNominateArbiter = () =>
needsArbiter && shouldShowArbiter && (
needsArbiter &&
shouldShowArbiter && (
<Alert
showIcon
type="warning"
@ -376,6 +411,8 @@ class ProposalDetailNaked extends React.Component<Props, State> {
<Card size="small" className="ProposalDetail-controls">
{renderCancelControl()}
{renderArbiterControl()}
{shouldShowChangeToAcceptedWithFunding &&
renderChangeToAcceptedWithFundingControl()}
</Card>
{/* DETAILS */}
@ -405,7 +442,10 @@ class ProposalDetailNaked extends React.Component<Props, State> {
{renderDeetItem('matching', p.contributionMatching)}
{renderDeetItem('bounty', p.contributionBounty)}
{renderDeetItem('rfpOptIn', JSON.stringify(p.rfpOptIn))}
{renderDeetItem('acceptedWithFunding', JSON.stringify(p.acceptedWithFunding))}
{renderDeetItem(
'acceptedWithFunding',
JSON.stringify(p.acceptedWithFunding),
)}
{renderDeetItem(
'arbiter',
<>
@ -460,6 +500,20 @@ class ProposalDetailNaked extends React.Component<Props, State> {
}
};
private handleChangeToAcceptedWithFunding = () => {
this.setState({ showChangeToAcceptedWithFundingPopover: true });
};
private handleChangeToAcceptWithFundingCancel = () => {
this.setState({ showChangeToAcceptedWithFundingPopover: false });
};
private handleChangeToAcceptWithFundingConfirm = () => {
if (!store.proposalDetail) return;
store.changeProposalToAcceptedWithFunding(store.proposalDetail.proposalId);
this.setState({ showChangeToAcceptedWithFundingPopover: false });
};
private getIdFromQuery = () => {
return Number(this.props.match.params.id);
};

View File

@ -148,6 +148,11 @@ async function cancelProposal(id: number) {
return data;
}
async function changeProposalToAcceptedWithFunding(id: number) {
const { data } = await api.put(`/admin/proposals/${id}/accept/fund`)
return data
}
async function fetchComments(params: Partial<PageQuery>) {
const { data } = await api.get('/admin/comments', { params });
return data;
@ -288,6 +293,7 @@ const app = store({
proposalDetailCanceling: false,
proposalDetailUpdating: false,
proposalDetailUpdated: false,
proposalDetailChangingToAcceptedWithFunding: false,
comments: {
page: createDefaultPageData<Comment>('CREATED:DESC'),
@ -571,6 +577,19 @@ const app = store({
app.proposalDetailCanceling = false;
},
async changeProposalToAcceptedWithFunding(id: number) {
app.proposalDetailChangingToAcceptedWithFunding = true
try {
const res = await changeProposalToAcceptedWithFunding(id)
app.updateProposalInStore(res)
} catch (e) {
handleApiError(e)
}
app.proposalDetailChangingToAcceptedWithFunding = false
},
async markMilestonePaid(proposalId: number, milestoneId: number, txId: string) {
app.proposalDetailMarkingMilestonePaid = true;
try {

View File

@ -369,6 +369,26 @@ def approve_proposal(id, is_accepted, with_funding, reject_reason=None):
return {"message": "No proposal found."}, 404
@blueprint.route('/proposals/<id>/accept/fund', methods=['PUT'])
@admin.admin_auth_required
def change_proposal_to_accepted_with_funding(id):
proposal = Proposal.query.filter_by(id=id).first()
if not proposal:
return {"message": "No proposal found."}, 404
if proposal.accepted_with_funding:
return {"message": "Proposal already accepted with funding."}, 404
if proposal.version != '2':
return {"message": "Only version two proposals can be accepted with funding"}, 404
if proposal.status != ProposalStatus.LIVE and proposal.status != ProposalStatus.APPROVED:
return {"message": "Only live or approved proposals can be modified by this endpoint"}, 404
proposal.update_proposal_with_funding()
db.session.add(proposal)
db.session.commit()
return proposal_schema.dump(proposal)
@blueprint.route('/proposals/<id>/cancel', methods=['PUT'])
@admin.admin_auth_required
def cancel_proposal(id):

View File

@ -542,6 +542,10 @@ class Proposal(db.Model):
'admin_note': reject_reason
})
def update_proposal_with_funding(self):
self.accepted_with_funding = True
self.fully_fund_contibution_bounty()
# state: status APPROVE -> LIVE, stage PREVIEW -> FUNDING_REQUIRED
def publish(self):
self.validate_publishable()

View File

@ -278,6 +278,55 @@ class TestAdminAPI(BaseProposalCreatorConfig):
self.assertEqual(resp.json["acceptedWithFunding"], False)
self.assertEqual(resp.json["contributionBounty"], "0")
@patch('requests.get', side_effect=mock_blockchain_api_requests)
def test_change_proposal_to_accepted_with_funding(self, mock_get):
self.login_admin()
# proposal needs to be PENDING
self.proposal.status = ProposalStatus.PENDING
# accept without funding
resp = self.app.put(
"/api/v1/admin/proposals/{}/accept".format(self.proposal.id),
data=json.dumps({"isAccepted": True, "withFunding": False})
)
self.assert200(resp)
self.assertEqual(resp.json["acceptedWithFunding"], False)
# change to accepted with funding
resp = self.app.put(
f"/api/v1/admin/proposals/{self.proposal.id}/accept/fund"
)
self.assert200(resp)
self.assertEqual(resp.json["acceptedWithFunding"], True)
# should fail if proposal is already accepted with funding
resp = self.app.put(
f"/api/v1/admin/proposals/{self.proposal.id}/accept/fund"
)
self.assert404(resp)
self.assertEqual(resp.json['message'], "Proposal already accepted with funding.")
self.proposal.accepted_with_funding = False
# should fail if proposal is not version two
self.proposal.version = ''
resp = self.app.put(
f"/api/v1/admin/proposals/{self.proposal.id}/accept/fund"
)
self.assert404(resp)
self.assertEqual(resp.json['message'], "Only version two proposals can be accepted with funding")
self.proposal.version = '2'
# should failed if proposal is not LIVE or APPROVED
self.proposal.status = ProposalStatus.PENDING
self.proposal.accepted_with_funding = False
resp = self.app.put(
f"/api/v1/admin/proposals/{self.proposal.id}/accept/fund"
)
self.assert404(resp)
self.assertEqual(resp.json["message"], 'Only live or approved proposals can be modified by this endpoint')
@patch('requests.get', side_effect=mock_blockchain_api_requests)
def test_reject_proposal(self, mock_get):