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:
parent
85c21d4cbf
commit
25e43a34ff
|
@ -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();
|
||||
|
@ -105,6 +107,39 @@ class ProposalDetailNaked extends React.Component<Props, State> {
|
|||
);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
const renderArbiterControl = () => (
|
||||
<ArbiterControl
|
||||
{...p}
|
||||
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue