Merge branch 'develop' into contribution-admin
This commit is contained in:
commit
33251d678c
|
@ -2,6 +2,7 @@ import datetime
|
|||
|
||||
from grant.extensions import ma, db
|
||||
from grant.utils.exceptions import ValidationException
|
||||
from grant.utils.misc import dt_to_unix
|
||||
|
||||
NOT_REQUESTED = 'NOT_REQUESTED'
|
||||
ONGOING_VOTE = 'ONGOING_VOTE'
|
||||
|
@ -64,6 +65,15 @@ class MilestoneSchema(ma.Schema):
|
|||
"date_created",
|
||||
)
|
||||
|
||||
date_created = ma.Method("get_date_created")
|
||||
date_estimated = ma.Method("get_date_estimated")
|
||||
|
||||
def get_date_created(self, obj):
|
||||
return dt_to_unix(obj.date_created)
|
||||
|
||||
def get_date_estimated(self, obj):
|
||||
return dt_to_unix(obj.date_estimated) if obj.date_estimated else None
|
||||
|
||||
|
||||
milestone_schema = MilestoneSchema()
|
||||
milestones_schema = MilestoneSchema(many=True)
|
||||
|
|
|
@ -2,6 +2,7 @@ import datetime
|
|||
from functools import reduce
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from decimal import Decimal
|
||||
|
||||
from grant.comment.models import Comment
|
||||
from grant.email.send import send_email
|
||||
|
@ -303,7 +304,7 @@ class Proposal(db.Model):
|
|||
self.deadline_duration = deadline_duration
|
||||
Proposal.validate(vars(self))
|
||||
|
||||
def create_contribution(self, user_id: int, amount: float):
|
||||
def create_contribution(self, user_id: int, amount):
|
||||
contribution = ProposalContribution(
|
||||
proposal_id=self.id,
|
||||
user_id=user_id,
|
||||
|
@ -315,18 +316,16 @@ class Proposal(db.Model):
|
|||
|
||||
def get_staking_contribution(self, user_id: int):
|
||||
contribution = None
|
||||
remaining = PROPOSAL_STAKING_AMOUNT - float(self.contributed)
|
||||
remaining = PROPOSAL_STAKING_AMOUNT - Decimal(self.contributed)
|
||||
# check funding
|
||||
if remaining > 0:
|
||||
# find pending contribution for any user
|
||||
# (always use full staking amout so we can find it)
|
||||
# find pending contribution for any user of remaining amount
|
||||
contribution = ProposalContribution.query.filter_by(
|
||||
proposal_id=self.id,
|
||||
amount=str(PROPOSAL_STAKING_AMOUNT),
|
||||
status=PENDING,
|
||||
).first()
|
||||
if not contribution:
|
||||
contribution = self.create_contribution(user_id, PROPOSAL_STAKING_AMOUNT)
|
||||
contribution = self.create_contribution(user_id, str(remaining.normalize()))
|
||||
|
||||
return contribution
|
||||
|
||||
|
@ -385,14 +384,14 @@ class Proposal(db.Model):
|
|||
contributions = ProposalContribution.query \
|
||||
.filter_by(proposal_id=self.id, status=ContributionStatus.CONFIRMED) \
|
||||
.all()
|
||||
funded = reduce(lambda prev, c: prev + float(c.amount), contributions, 0)
|
||||
funded = reduce(lambda prev, c: prev + Decimal(c.amount), contributions, 0)
|
||||
return str(funded)
|
||||
|
||||
@hybrid_property
|
||||
def funded(self):
|
||||
target = float(self.target)
|
||||
target = Decimal(self.target)
|
||||
# apply matching multiplier
|
||||
funded = float(self.contributed) * (1 + self.contribution_matching)
|
||||
funded = Decimal(self.contributed) * Decimal(1 + self.contribution_matching)
|
||||
# if funded > target, just set as target
|
||||
if funded > target:
|
||||
return str(target)
|
||||
|
@ -401,7 +400,7 @@ class Proposal(db.Model):
|
|||
|
||||
@hybrid_property
|
||||
def is_staked(self):
|
||||
return float(self.contributed) >= PROPOSAL_STAKING_AMOUNT
|
||||
return Decimal(self.contributed) >= PROPOSAL_STAKING_AMOUNT
|
||||
|
||||
|
||||
class ProposalSchema(ma.Schema):
|
||||
|
|
|
@ -237,6 +237,7 @@ def delete_proposal(proposal_id):
|
|||
ProposalStatus.PENDING,
|
||||
ProposalStatus.APPROVED,
|
||||
ProposalStatus.REJECTED,
|
||||
ProposalStatus.STAKING,
|
||||
]
|
||||
status = g.current_proposal.status
|
||||
if status not in deleteable_statuses:
|
||||
|
@ -482,7 +483,7 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
|
|||
|
||||
if contribution.proposal.status == ProposalStatus.STAKING:
|
||||
# fully staked, set status PENDING & notify user
|
||||
if contribution.proposal.is_staked: # float(contribution.proposal.contributed) >= PROPOSAL_STAKING_AMOUNT:
|
||||
if contribution.proposal.is_staked: # Decimal(contribution.proposal.contributed) >= PROPOSAL_STAKING_AMOUNT:
|
||||
contribution.proposal.status = ProposalStatus.PENDING
|
||||
db.session.add(contribution.proposal)
|
||||
db.session.commit()
|
||||
|
@ -493,7 +494,7 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
|
|||
'proposal': contribution.proposal,
|
||||
'tx_explorer_url': f'{EXPLORER_URL}transactions/{txid}',
|
||||
'fully_staked': contribution.proposal.is_staked,
|
||||
'stake_target': PROPOSAL_STAKING_AMOUNT
|
||||
'stake_target': str(PROPOSAL_STAKING_AMOUNT.normalize()),
|
||||
})
|
||||
|
||||
else:
|
||||
|
|
|
@ -7,6 +7,7 @@ For local development, use a .env file to set
|
|||
environment variables.
|
||||
"""
|
||||
from environs import Env
|
||||
from decimal import Decimal
|
||||
|
||||
env = Env()
|
||||
env.read_env()
|
||||
|
@ -54,7 +55,7 @@ ADMIN_PASS_HASH = env.str("ADMIN_PASS_HASH")
|
|||
|
||||
EXPLORER_URL = env.str("EXPLORER_URL", default="https://explorer.zcha.in/")
|
||||
|
||||
PROPOSAL_STAKING_AMOUNT = env.float("PROPOSAL_STAKING_AMOUNT")
|
||||
PROPOSAL_STAKING_AMOUNT = Decimal(env.str("PROPOSAL_STAKING_AMOUNT"))
|
||||
|
||||
UI = {
|
||||
'NAME': 'ZF Grants',
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
will now be forwarded to administrators for approval.
|
||||
{% else %}
|
||||
<strong>{{ args.proposal.title }}</strong> has been partially staked for
|
||||
<strong>{{ args.contribution.amount }} ZEC</strong>. This is not enough to
|
||||
fully stake the proposal. You must send at least
|
||||
<strong>{{ args.stake_target }} ZEC</strong>.
|
||||
<strong>{{ args.contribution.amount }} ZEC</strong> of the required
|
||||
<strong>{{ args.stake_target}} ZEC</strong>.
|
||||
You can send the remaining amount by going to your profile's "Pending" tab,
|
||||
and clicking the "Stake" button next to the proposal.
|
||||
{% endif %}
|
||||
You can view your transaction below:
|
||||
</p>
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
Your proposal will now be forwarded to administrators for approval.
|
||||
{% else %}
|
||||
{{ args.proposal.title }} has been partially staked for
|
||||
{{ args.contribution.amount }} ZEC. This is not enough to
|
||||
fully stake the proposal. You must send at least
|
||||
{{ args.stake_target }} ZEC.
|
||||
{{ args.contribution.amount }} ZEC of the required {{ args.stake_target}} ZEC.
|
||||
You can send the remaining amount by going to your profile's "Pending" tab,
|
||||
and clicking the "Stake" button next to the proposal.
|
||||
{% endif %}
|
||||
You can view your transaction here:
|
||||
|
||||
|
|
|
@ -155,7 +155,7 @@ class BaseProposalCreatorConfig(BaseUserConfig):
|
|||
# 2. get staking contribution
|
||||
contribution = self.proposal.get_staking_contribution(self.user.id)
|
||||
# 3. fake a confirmation
|
||||
contribution.confirm(tx_id='tx', amount=str(PROPOSAL_STAKING_AMOUNT))
|
||||
contribution.confirm(tx_id='tx', amount=str(PROPOSAL_STAKING_AMOUNT.normalize()))
|
||||
db.session.add(contribution)
|
||||
db.session.commit()
|
||||
contribution = self.proposal.get_staking_contribution(self.user.id)
|
||||
|
|
|
@ -115,7 +115,7 @@ class TestProposalAPI(BaseProposalCreatorConfig):
|
|||
resp = self.app.get(f"/api/v1/proposals/{self.proposal.id}/stake")
|
||||
print(resp)
|
||||
self.assert200(resp)
|
||||
self.assertEquals(resp.json['amount'], str(PROPOSAL_STAKING_AMOUNT))
|
||||
self.assertEquals(resp.json['amount'], str(PROPOSAL_STAKING_AMOUNT.normalize()))
|
||||
|
||||
@patch('requests.get', side_effect=mock_blockchain_api_requests)
|
||||
def test_proposal_stake_no_auth(self, mock_get):
|
||||
|
|
|
@ -8,10 +8,12 @@ import PaymentInfo from './PaymentInfo';
|
|||
|
||||
interface OwnProps {
|
||||
isVisible: boolean;
|
||||
contribution?: ContributionWithAddresses | Falsy;
|
||||
proposalId?: number;
|
||||
contributionId?: number;
|
||||
amount?: string;
|
||||
hasNoButtons?: boolean;
|
||||
text?: React.ReactNode;
|
||||
handleClose(): void;
|
||||
}
|
||||
|
||||
|
@ -30,22 +32,32 @@ export default class ContributionModal extends React.Component<Props, State> {
|
|||
error: null,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
if (props.contribution) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
contribution: props.contribution,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps: Props) {
|
||||
const { isVisible, proposalId, contributionId } = nextProps;
|
||||
const { isVisible, proposalId, contributionId, contribution } = nextProps;
|
||||
// When modal is opened and proposalId is provided or changed
|
||||
if (isVisible && proposalId) {
|
||||
if (
|
||||
this.props.isVisible !== isVisible ||
|
||||
proposalId !== this.props.proposalId
|
||||
) {
|
||||
if (this.props.isVisible !== isVisible || proposalId !== this.props.proposalId) {
|
||||
this.fetchAddresses(proposalId, contributionId);
|
||||
}
|
||||
}
|
||||
|
||||
// If contribution is provided
|
||||
if (contribution !== this.props.contribution) {
|
||||
this.setState({ contribution: contribution || null });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isVisible, handleClose, hasNoButtons } = this.props;
|
||||
const { isVisible, handleClose, hasNoButtons, text } = this.props;
|
||||
const { hasSent, contribution, error } = this.state;
|
||||
let content;
|
||||
|
||||
|
@ -68,7 +80,7 @@ export default class ContributionModal extends React.Component<Props, State> {
|
|||
if (error) {
|
||||
content = error;
|
||||
} else {
|
||||
content = <PaymentInfo contribution={contribution} />;
|
||||
content = <PaymentInfo contribution={contribution} text={text} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,22 +101,16 @@ export default class ContributionModal extends React.Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
private async fetchAddresses(
|
||||
proposalId: number,
|
||||
contributionId?: number,
|
||||
) {
|
||||
private async fetchAddresses(proposalId: number, contributionId?: number) {
|
||||
try {
|
||||
let res;
|
||||
if (contributionId) {
|
||||
res = await getProposalContribution(proposalId, contributionId);
|
||||
} else {
|
||||
res = await postProposalContribution(
|
||||
proposalId,
|
||||
this.props.amount || '0',
|
||||
);
|
||||
res = await postProposalContribution(proposalId, this.props.amount || '0');
|
||||
}
|
||||
this.setState({ contribution: res.data });
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
this.setState({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ const DEFAULT_STATE: State = {
|
|||
{
|
||||
title: '',
|
||||
content: '',
|
||||
dateEstimated: '',
|
||||
dateEstimated: moment().unix(),
|
||||
payoutPercent: '100',
|
||||
immediatePayout: false,
|
||||
},
|
||||
|
@ -159,10 +159,21 @@ const MilestoneFields = ({
|
|||
<DatePicker.MonthPicker
|
||||
style={{ flex: 1, marginRight: '0.5rem' }}
|
||||
placeholder="Expected completion date"
|
||||
value={milestone.dateEstimated ? moment(milestone.dateEstimated) : undefined}
|
||||
value={
|
||||
milestone.dateEstimated ? moment(milestone.dateEstimated * 1000) : undefined
|
||||
}
|
||||
format="MMMM YYYY"
|
||||
allowClear={false}
|
||||
onChange={(_, dateEstimated) => onChange(index, { ...milestone, dateEstimated })}
|
||||
onChange={time => onChange(index, { ...milestone, dateEstimated: time.unix() })}
|
||||
disabled={milestone.immediatePayout}
|
||||
disabledDate={current =>
|
||||
current
|
||||
? current <
|
||||
moment()
|
||||
.subtract(1, 'month')
|
||||
.endOf('month')
|
||||
: false
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
min={1}
|
||||
|
@ -186,6 +197,9 @@ const MilestoneFields = ({
|
|||
onChange(index, {
|
||||
...milestone,
|
||||
immediatePayout: ev.target.checked,
|
||||
dateEstimated: ev.target.checked
|
||||
? moment().unix()
|
||||
: milestone.dateEstimated,
|
||||
})
|
||||
}
|
||||
>
|
||||
|
|
|
@ -182,7 +182,7 @@ const ReviewMilestones = ({
|
|||
<div className="ReviewMilestone">
|
||||
<div className="ReviewMilestone-title">{m.title}</div>
|
||||
<div className="ReviewMilestone-info">
|
||||
{moment(m.dateEstimated, 'MMMM YYYY').format('MMMM YYYY')}
|
||||
{moment(m.dateEstimated * 1000).format('MMMM YYYY')}
|
||||
{' – '}
|
||||
{m.payoutPercent}% of funds
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import moment from 'moment';
|
||||
import { PROPOSAL_CATEGORY } from 'api/constants';
|
||||
import { ProposalDraft } from 'types';
|
||||
|
||||
|
@ -19,7 +20,9 @@ const createExampleProposal = (): Partial<ProposalDraft> => {
|
|||
title: 'Initial Funding',
|
||||
content:
|
||||
'This will be used to pay for a professional designer to hand-craft each letter on the shirt.',
|
||||
dateEstimated: 'October 2018',
|
||||
dateEstimated: moment()
|
||||
.add(1, 'month')
|
||||
.unix(),
|
||||
payoutPercent: '30',
|
||||
immediatePayout: true,
|
||||
},
|
||||
|
@ -27,7 +30,9 @@ const createExampleProposal = (): Partial<ProposalDraft> => {
|
|||
title: 'Test Prints',
|
||||
content:
|
||||
"We'll get test prints from 5 different factories and choose the highest quality shirts. Once we've decided, we'll order a full batch of prints.",
|
||||
dateEstimated: 'November 2018',
|
||||
dateEstimated: moment()
|
||||
.add(2, 'month')
|
||||
.unix(),
|
||||
payoutPercent: '20',
|
||||
immediatePayout: false,
|
||||
},
|
||||
|
@ -35,7 +40,9 @@ const createExampleProposal = (): Partial<ProposalDraft> => {
|
|||
title: 'All Shirts Printed',
|
||||
content:
|
||||
"All of the shirts have been printed, hooray! They'll be given out at conferences and meetups.",
|
||||
dateEstimated: 'December 2018',
|
||||
dateEstimated: moment()
|
||||
.add(3, 'month')
|
||||
.unix(),
|
||||
payoutPercent: '50',
|
||||
immediatePayout: false,
|
||||
},
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Popconfirm, message, Tag } from 'antd';
|
||||
import { UserProposal, STATUS } from 'types';
|
||||
import { UserProposal, STATUS, ContributionWithAddresses } from 'types';
|
||||
import ContributionModal from 'components/ContributionModal';
|
||||
import { getProposalStakingContribution } from 'api/api';
|
||||
import { deletePendingProposal, publishPendingProposal } from 'modules/users/actions';
|
||||
import './ProfilePending.less';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import './ProfilePending.less';
|
||||
|
||||
interface OwnProps {
|
||||
proposal: UserProposal;
|
||||
onPublish: (id: UserProposal['proposalId']) => void;
|
||||
onPublish(id: UserProposal['proposalId']): void;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
|
@ -23,18 +25,24 @@ interface DispatchProps {
|
|||
|
||||
type Props = OwnProps & StateProps & DispatchProps;
|
||||
|
||||
const STATE = {
|
||||
isDeleting: false,
|
||||
isPublishing: false,
|
||||
};
|
||||
|
||||
type State = typeof STATE;
|
||||
interface State {
|
||||
isDeleting: boolean;
|
||||
isPublishing: boolean;
|
||||
isLoadingStake: boolean;
|
||||
stakeContribution: ContributionWithAddresses | null;
|
||||
}
|
||||
|
||||
class ProfilePending extends React.Component<Props, State> {
|
||||
state = STATE;
|
||||
state: State = {
|
||||
isDeleting: false,
|
||||
isPublishing: false,
|
||||
isLoadingStake: false,
|
||||
stakeContribution: null,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { status, title, proposalId, rejectReason } = this.props.proposal;
|
||||
const { isDeleting, isPublishing } = this.state;
|
||||
const { isDeleting, isPublishing, isLoadingStake, stakeContribution } = this.state;
|
||||
|
||||
const isDisableActions = isDeleting || isPublishing;
|
||||
|
||||
|
@ -105,6 +113,15 @@ class ProfilePending extends React.Component<Props, State> {
|
|||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
{STATUS.STAKING === status && (
|
||||
<Button
|
||||
type="primary"
|
||||
loading={isLoadingStake}
|
||||
onClick={this.openStakingModal}
|
||||
>
|
||||
Stake
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
|
@ -116,6 +133,22 @@ class ProfilePending extends React.Component<Props, State> {
|
|||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
|
||||
{STATUS.STAKING && (
|
||||
<ContributionModal
|
||||
isVisible={!!stakeContribution}
|
||||
contribution={stakeContribution}
|
||||
handleClose={this.closeStakingModal}
|
||||
text={
|
||||
<p>
|
||||
Please send the staking contribution of{' '}
|
||||
<b>{stakeContribution && stakeContribution.amount} ZEC</b> using the
|
||||
instructions below. Once your payment has been sent and confirmed, you
|
||||
will receive an email.
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -152,6 +185,25 @@ class ProfilePending extends React.Component<Props, State> {
|
|||
this.setState({ isDeleting: false });
|
||||
}
|
||||
};
|
||||
|
||||
private openStakingModal = async () => {
|
||||
try {
|
||||
this.setState({ isLoadingStake: true });
|
||||
const res = await getProposalStakingContribution(this.props.proposal.proposalId);
|
||||
this.setState({ stakeContribution: res.data }, () => {
|
||||
this.setState({ isLoadingStake: false });
|
||||
});
|
||||
} catch (err) {
|
||||
message.error(err.message, 3);
|
||||
this.setState({ isLoadingStake: false });
|
||||
}
|
||||
};
|
||||
|
||||
private closeStakingModal = () =>
|
||||
this.setState({
|
||||
isLoadingStake: false,
|
||||
stakeContribution: null,
|
||||
});
|
||||
}
|
||||
|
||||
export default connect<StateProps, DispatchProps, OwnProps, AppState>(
|
||||
|
|
|
@ -49,8 +49,9 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
|||
let content;
|
||||
if (proposal) {
|
||||
const { target, funded, percentFunded } = proposal;
|
||||
const datePublished = proposal.datePublished || Date.now() / 1000;
|
||||
const isRaiseGoalReached = funded.gte(target);
|
||||
const deadline = (proposal.dateCreated + proposal.deadlineDuration) * 1000;
|
||||
const deadline = (datePublished + proposal.deadlineDuration) * 1000;
|
||||
// TODO: Get values from proposal
|
||||
console.warn('TODO: Get isFrozen from proposal data');
|
||||
const isFrozen = false;
|
||||
|
@ -66,7 +67,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
|||
<div className="ProposalCampaignBlock-info">
|
||||
<div className="ProposalCampaignBlock-info-label">Started</div>
|
||||
<div className="ProposalCampaignBlock-info-value">
|
||||
{moment(proposal.datePublished * 1000).fromNow()}
|
||||
{moment(datePublished * 1000).fromNow()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -98,7 +98,7 @@ class ProposalMilestones extends React.Component<Props, State> {
|
|||
: milestoneStateToStepState[milestone.state];
|
||||
|
||||
const className = this.state.step === i ? 'is-active' : 'is-inactive';
|
||||
const estimatedDate = moment(milestone.dateEstimated).format('MMMM YYYY');
|
||||
const estimatedDate = moment(milestone.dateEstimated * 1000).format('MMMM YYYY');
|
||||
const reward = (
|
||||
<UnitDisplay value={milestone.amount} symbol="ZEC" displayShortBalance={4} />
|
||||
);
|
||||
|
@ -121,7 +121,7 @@ class ProposalMilestones extends React.Component<Props, State> {
|
|||
message={
|
||||
<span>
|
||||
The team was awarded <strong>{reward}</strong>{' '}
|
||||
{milestone.isImmediatePayout
|
||||
{milestone.immediatePayout
|
||||
? 'as an initial payout'
|
||||
: // TODO: Add property for payout date on milestones
|
||||
`on ${moment().format('MMM Do, YYYY')}`}
|
||||
|
@ -164,7 +164,7 @@ class ProposalMilestones extends React.Component<Props, State> {
|
|||
|
||||
const statuses = (
|
||||
<div className="ProposalMilestones-milestone-status">
|
||||
{!milestone.isImmediatePayout && (
|
||||
{!milestone.immediatePayout && (
|
||||
<div>
|
||||
Estimate: <strong>{estimatedDate}</strong>
|
||||
</div>
|
||||
|
|
|
@ -19,6 +19,7 @@ export class ProposalCard extends React.Component<Proposal> {
|
|||
proposalAddress,
|
||||
proposalUrlId,
|
||||
category,
|
||||
datePublished,
|
||||
dateCreated,
|
||||
team,
|
||||
target,
|
||||
|
@ -28,11 +29,7 @@ export class ProposalCard extends React.Component<Proposal> {
|
|||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="ProposalCard"
|
||||
to={`/proposals/${proposalUrlId}`}
|
||||
title={title}
|
||||
>
|
||||
<Card className="ProposalCard" to={`/proposals/${proposalUrlId}`} title={title}>
|
||||
{contributionMatching > 0 && (
|
||||
<div className="ProposalCard-ribbon">
|
||||
<span>
|
||||
|
@ -77,7 +74,7 @@ export class ProposalCard extends React.Component<Proposal> {
|
|||
</div>
|
||||
</div>
|
||||
<div className="ProposalCard-address">{proposalAddress}</div>
|
||||
<Card.Info category={category} time={dateCreated * 1000} />
|
||||
<Card.Info category={category} time={(datePublished || dateCreated) * 1000} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -183,6 +183,7 @@ export function makeProposalPreviewFromDraft(draft: ProposalDraft): Proposal {
|
|||
payoutAddress: '0x0',
|
||||
dateCreated: Date.now() / 1000,
|
||||
datePublished: Date.now() / 1000,
|
||||
dateApproved: Date.now() / 1000,
|
||||
deadlineDuration: 86400 * 60,
|
||||
target: toZat(draft.target),
|
||||
funded: Zat('0'),
|
||||
|
@ -198,7 +199,6 @@ export function makeProposalPreviewFromDraft(draft: ProposalDraft): Proposal {
|
|||
amount: toZat(target * (parseInt(m.payoutPercent, 10) / 100)),
|
||||
dateEstimated: m.dateEstimated,
|
||||
immediatePayout: m.immediatePayout,
|
||||
isImmediatePayout: m.immediatePayout,
|
||||
isPaid: false,
|
||||
payoutPercent: m.payoutPercent.toString(),
|
||||
state: MILESTONE_STATE.WAITING,
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from 'types';
|
||||
import { PROPOSAL_CATEGORY } from 'api/constants';
|
||||
import BN from 'bn.js';
|
||||
import moment from 'moment';
|
||||
|
||||
const oneZec = new BN('100000000');
|
||||
|
||||
|
@ -101,23 +102,22 @@ export function generateProposal({
|
|||
const genMilestone = (
|
||||
overrides: Partial<ProposalMilestone> = {},
|
||||
): ProposalMilestone => {
|
||||
const now = new Date();
|
||||
if (overrides.index) {
|
||||
const estimate = new Date(now.setMonth(now.getMonth() + overrides.index));
|
||||
overrides.dateEstimated = estimate.toISOString();
|
||||
overrides.dateEstimated = moment()
|
||||
.add(overrides.index, 'month')
|
||||
.unix();
|
||||
}
|
||||
|
||||
const defaults: ProposalMilestone = {
|
||||
title: 'Milestone A',
|
||||
content: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
|
||||
tempor incididunt ut labore et dolore magna aliqua.`,
|
||||
dateEstimated: '2018-10-01T00:00:00+00:00',
|
||||
dateEstimated: moment().unix(),
|
||||
immediatePayout: true,
|
||||
index: 0,
|
||||
state: MILESTONE_STATE.WAITING,
|
||||
amount: amountBn,
|
||||
isPaid: false,
|
||||
isImmediatePayout: true,
|
||||
payoutPercent: '33',
|
||||
};
|
||||
return { ...defaults, ...overrides };
|
||||
|
@ -128,7 +128,6 @@ export function generateProposal({
|
|||
index: i,
|
||||
title: genMilestoneTitle(),
|
||||
immediatePayout: i === 0,
|
||||
isImmediatePayout: i === 0,
|
||||
payoutRequestVoteDeadline: i !== 0 ? Date.now() + 3600000 : 0,
|
||||
payoutPercent: '' + (1 / milestoneCount) * 100,
|
||||
};
|
||||
|
@ -149,6 +148,7 @@ export function generateProposal({
|
|||
payoutAddress: 'z123',
|
||||
dateCreated: created / 1000,
|
||||
datePublished: created / 1000,
|
||||
dateApproved: created / 1000,
|
||||
deadlineDuration: 86400 * 60,
|
||||
target: amountBn,
|
||||
funded: fundedBn,
|
||||
|
|
|
@ -12,13 +12,12 @@ export interface Milestone {
|
|||
state: MILESTONE_STATE;
|
||||
amount: Zat;
|
||||
isPaid: boolean;
|
||||
isImmediatePayout: boolean;
|
||||
immediatePayout: boolean;
|
||||
dateEstimated: number;
|
||||
}
|
||||
|
||||
export interface ProposalMilestone extends Milestone {
|
||||
content: string;
|
||||
immediatePayout: boolean;
|
||||
dateEstimated: string;
|
||||
payoutPercent: string;
|
||||
title: string;
|
||||
}
|
||||
|
@ -26,7 +25,7 @@ export interface ProposalMilestone extends Milestone {
|
|||
export interface CreateMilestone {
|
||||
title: string;
|
||||
content: string;
|
||||
dateEstimated: string;
|
||||
dateEstimated: number;
|
||||
payoutPercent: string;
|
||||
immediatePayout: boolean;
|
||||
}
|
||||
|
|
|
@ -47,7 +47,8 @@ export interface Proposal extends Omit<ProposalDraft, 'target' | 'invites'> {
|
|||
percentFunded: number;
|
||||
contributionMatching: number;
|
||||
milestones: ProposalMilestone[];
|
||||
datePublished: number;
|
||||
datePublished: number | null;
|
||||
dateApproved: number | null;
|
||||
}
|
||||
|
||||
export interface TeamInviteWithProposal extends TeamInvite {
|
||||
|
|
Loading…
Reference in New Issue