commit
dd966ae853
|
@ -89,7 +89,7 @@
|
||||||
"react-copy-to-clipboard": "^5.0.1",
|
"react-copy-to-clipboard": "^5.0.1",
|
||||||
"react-dev-utils": "^5.0.2",
|
"react-dev-utils": "^5.0.2",
|
||||||
"react-dom": "16.5.2",
|
"react-dom": "16.5.2",
|
||||||
"react-easy-state": "^6.0.4",
|
"react-easy-state": "^6.1.3",
|
||||||
"react-hot-loader": "^4.3.8",
|
"react-hot-loader": "^4.3.8",
|
||||||
"react-router": "^4.3.1",
|
"react-router": "^4.3.1",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
|
|
|
@ -36,23 +36,27 @@ class ContributionDetail extends React.Component<Props, State> {
|
||||||
|
|
||||||
const renderDeetItem = (label: string, val: React.ReactNode) => (
|
const renderDeetItem = (label: string, val: React.ReactNode) => (
|
||||||
<div className="ContributionDetail-deet">
|
<div className="ContributionDetail-deet">
|
||||||
<div className="ContributionDetail-deet-value">
|
<div className="ContributionDetail-deet-value">{val}</div>
|
||||||
{val}
|
<div className="ContributionDetail-deet-label">{label}</div>
|
||||||
</div>
|
|
||||||
<div className="ContributionDetail-deet-label">
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderSendRefund = () => {
|
const renderSendRefund = () => {
|
||||||
if (c.staking || !c.refundAddress || c.refundTxId || !c.proposal.isFailed || !c.user) {
|
if (
|
||||||
|
c.staking ||
|
||||||
|
!c.refundAddress ||
|
||||||
|
c.refundTxId ||
|
||||||
|
!c.proposal.isFailed ||
|
||||||
|
!c.user
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const percent = c.proposal.milestones.reduce((prev, m) => {
|
const percent = c.proposal.milestones.reduce((prev, m) => {
|
||||||
return m.datePaid ? prev - parseFloat(m.payoutPercent) : prev;
|
return m.datePaid ? prev - parseFloat(m.payoutPercent) : prev;
|
||||||
}, 100);
|
}, 100);
|
||||||
const amount = toZat(c.amount).muln(percent).divn(100);
|
const amount = toZat(c.amount)
|
||||||
|
.muln(percent)
|
||||||
|
.divn(100);
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
className="ContributionDetail-alert"
|
className="ContributionDetail-alert"
|
||||||
|
@ -62,15 +66,15 @@ class ContributionDetail extends React.Component<Props, State> {
|
||||||
description={
|
description={
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
The proposal this contribution was made towards has failed, and
|
The proposal this contribution was made towards has failed, and is ready
|
||||||
is ready to be refunded to <strong>{c.user.displayName}</strong>
|
to be refunded to <strong>{c.user.displayName}</strong> for{' '}
|
||||||
{' '}for <strong>{percent}%</strong> of the contribution amount.
|
<strong>{percent}%</strong> of the contribution amount. Please Make a
|
||||||
Please Make a payment of <strong>{fromZat(amount)} ZEC</strong> to:
|
payment of <strong>{fromZat(amount)} ZEC</strong> to:
|
||||||
</p>
|
</p>
|
||||||
<pre>{c.refundAddress}</pre>
|
<pre>{c.refundAddress}</pre>
|
||||||
<p>
|
<p>
|
||||||
They will be sent an email notifying them of the refund when you
|
They will be sent an email notifying them of the refund when you enter the
|
||||||
enter the txid below.
|
txid below.
|
||||||
</p>
|
</p>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder="Enter payment txid"
|
placeholder="Enter payment txid"
|
||||||
|
@ -98,7 +102,6 @@ class ContributionDetail extends React.Component<Props, State> {
|
||||||
<pre>{JSON.stringify(c.addresses, null, 4)}</pre>
|
<pre>{JSON.stringify(c.addresses, null, 4)}</pre>
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
|
|
||||||
|
|
||||||
<Collapse.Panel key="user" header="user">
|
<Collapse.Panel key="user" header="user">
|
||||||
{c.user ? <UserItem {...c.user} /> : <em>Anonymous contribution</em>}
|
{c.user ? <UserItem {...c.user} /> : <em>Anonymous contribution</em>}
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
|
@ -118,7 +121,9 @@ class ContributionDetail extends React.Component<Props, State> {
|
||||||
{/* ACTIONS */}
|
{/* ACTIONS */}
|
||||||
<Card size="small" className="ContributionDetail-controls">
|
<Card size="small" className="ContributionDetail-controls">
|
||||||
<Link to={`/contributions/${id}/edit`}>
|
<Link to={`/contributions/${id}/edit`}>
|
||||||
<Button type="primary" block>Edit</Button>
|
<Button type="primary" block>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
@ -128,16 +133,20 @@ class ContributionDetail extends React.Component<Props, State> {
|
||||||
{renderDeetItem('created', formatDateSeconds(c.dateCreated))}
|
{renderDeetItem('created', formatDateSeconds(c.dateCreated))}
|
||||||
{renderDeetItem('status', c.status)}
|
{renderDeetItem('status', c.status)}
|
||||||
{renderDeetItem('amount', c.amount)}
|
{renderDeetItem('amount', c.amount)}
|
||||||
{renderDeetItem('txid', c.txId
|
{renderDeetItem(
|
||||||
? <Input size="small" value={c.txId} readOnly />
|
'txid',
|
||||||
: <em>N/A</em>
|
c.txId ? <Input size="small" value={c.txId} readOnly /> : <em>N/A</em>,
|
||||||
)}
|
)}
|
||||||
{renderDeetItem('refund txid', c.refundTxId
|
{renderDeetItem(
|
||||||
? <Input size="small" value={c.refundTxId} readOnly />
|
'refund txid',
|
||||||
: <em>N/A</em>
|
c.refundTxId ? (
|
||||||
|
<Input size="small" value={c.refundTxId} readOnly />
|
||||||
|
) : (
|
||||||
|
<em>N/A</em>
|
||||||
|
),
|
||||||
)}
|
)}
|
||||||
{renderDeetItem('staking tx', JSON.stringify(c.staking))}
|
{renderDeetItem('staking tx', JSON.stringify(c.staking))}
|
||||||
{renderDeetItem('no refund', JSON.stringify(c.noRefund))}
|
{renderDeetItem('private', JSON.stringify(c.private))}
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -152,7 +152,7 @@ export interface Contribution {
|
||||||
memo: string;
|
memo: string;
|
||||||
};
|
};
|
||||||
staking: boolean;
|
staking: boolean;
|
||||||
noRefund: boolean;
|
private: boolean;
|
||||||
refundAddress?: string;
|
refundAddress?: string;
|
||||||
refundTxId?: string;
|
refundTxId?: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -792,9 +792,10 @@
|
||||||
reflect-metadata "^0.1.12"
|
reflect-metadata "^0.1.12"
|
||||||
tslib "^1.8.1"
|
tslib "^1.8.1"
|
||||||
|
|
||||||
"@nx-js/observer-util@^4.1.1":
|
"@nx-js/observer-util@^4.2.2":
|
||||||
version "4.2.0"
|
version "4.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@nx-js/observer-util/-/observer-util-4.2.0.tgz#ec9e2f903dda94cc3d8ac077617bc369f2ad2d6e"
|
resolved "https://registry.yarnpkg.com/@nx-js/observer-util/-/observer-util-4.2.2.tgz#faea1bc61936653486d1b5dec7485220991e628d"
|
||||||
|
integrity sha512-9OayX1xkdGjdnsDiO2YdaYJ6aMyCF7/NY4QWVgIgjSAZJ4OX2fD766Ts79hEzBscenQy2DCaSoY8VkguIMB1ZA==
|
||||||
|
|
||||||
"@samverschueren/stream-to-observable@^0.3.0":
|
"@samverschueren/stream-to-observable@^0.3.0":
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
|
@ -6616,11 +6617,12 @@ react-dom@16.5.2:
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
schedule "^0.5.0"
|
schedule "^0.5.0"
|
||||||
|
|
||||||
react-easy-state@^6.0.4:
|
react-easy-state@^6.1.3:
|
||||||
version "6.0.4"
|
version "6.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-easy-state/-/react-easy-state-6.0.4.tgz#94a124fe69723abcb922c15059dd444b2e3499f3"
|
resolved "https://registry.yarnpkg.com/react-easy-state/-/react-easy-state-6.1.3.tgz#f9db4e8d842b5acfb73b6899aaf49a26900f7d26"
|
||||||
|
integrity sha512-uWQ7ittvJylwn/Xgz7Ub1jjsbpthQ9Ad1KDLxXfbXCb2OPnov4porVdnOJU2PKeRezcam3ZgfPUtf9L9rjtyWg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@nx-js/observer-util" "^4.1.1"
|
"@nx-js/observer-util" "^4.2.2"
|
||||||
|
|
||||||
react-error-overlay@^4.0.1:
|
react-error-overlay@^4.0.1:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
|
|
|
@ -156,7 +156,6 @@ def stats():
|
||||||
contribution_refundable_count = db.session.query(func.count(ProposalContribution.id)) \
|
contribution_refundable_count = db.session.query(func.count(ProposalContribution.id)) \
|
||||||
.filter(ProposalContribution.refund_tx_id == None) \
|
.filter(ProposalContribution.refund_tx_id == None) \
|
||||||
.filter(ProposalContribution.staking == False) \
|
.filter(ProposalContribution.staking == False) \
|
||||||
.filter(ProposalContribution.no_refund == False) \
|
|
||||||
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
|
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
|
||||||
.join(Proposal) \
|
.join(Proposal) \
|
||||||
.filter(or_(
|
.filter(or_(
|
||||||
|
@ -609,7 +608,7 @@ def get_contribution(contribution_id):
|
||||||
@body({
|
@body({
|
||||||
"proposalId": fields.Int(required=False, missing=None),
|
"proposalId": fields.Int(required=False, missing=None),
|
||||||
"userId": fields.Int(required=False, missing=None),
|
"userId": fields.Int(required=False, missing=None),
|
||||||
"status": fields.Str(required=True, validate=validate.OneOf(choices=ContributionStatus.list())),
|
"status": fields.Str(required=False, missing=None, validate=validate.OneOf(choices=ContributionStatus.list())),
|
||||||
"amount": fields.Str(required=False, missing=None),
|
"amount": fields.Str(required=False, missing=None),
|
||||||
"txId": fields.Str(required=False, missing=None),
|
"txId": fields.Str(required=False, missing=None),
|
||||||
"refundTxId": fields.Str(required=False, allow_none=True, missing=None),
|
"refundTxId": fields.Str(required=False, allow_none=True, missing=None),
|
||||||
|
@ -723,6 +722,8 @@ def financials():
|
||||||
SELECT SUM(TO_NUMBER(amount, '{nfmt}'))
|
SELECT SUM(TO_NUMBER(amount, '{nfmt}'))
|
||||||
FROM proposal_contribution as pc
|
FROM proposal_contribution as pc
|
||||||
INNER JOIN proposal as p ON pc.proposal_id = p.id
|
INNER JOIN proposal as p ON pc.proposal_id = p.id
|
||||||
|
LEFT OUTER JOIN "user" as u ON pc.user_id = u.id
|
||||||
|
LEFT OUTER JOIN user_settings as us ON u.id = us.user_id
|
||||||
WHERE {where}
|
WHERE {where}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -743,14 +744,34 @@ def financials():
|
||||||
'staking': str(ex(sql_pc("status = 'CONFIRMED' AND staking = TRUE"))),
|
'staking': str(ex(sql_pc("status = 'CONFIRMED' AND staking = TRUE"))),
|
||||||
'funding': str(ex(sql_pc_p("pc.status = 'CONFIRMED' AND pc.staking = FALSE AND p.stage = 'FUNDING_REQUIRED'"))),
|
'funding': str(ex(sql_pc_p("pc.status = 'CONFIRMED' AND pc.staking = FALSE AND p.stage = 'FUNDING_REQUIRED'"))),
|
||||||
'funded': str(ex(sql_pc_p("pc.status = 'CONFIRMED' AND pc.staking = FALSE AND p.stage in ('WIP', 'COMPLETED')"))),
|
'funded': str(ex(sql_pc_p("pc.status = 'CONFIRMED' AND pc.staking = FALSE AND p.stage in ('WIP', 'COMPLETED')"))),
|
||||||
|
# should have a refund_address
|
||||||
'refunding': str(ex(sql_pc_p(
|
'refunding': str(ex(sql_pc_p(
|
||||||
"pc.status = 'CONFIRMED' AND pc.staking = FALSE AND pc.no_refund = FALSE AND pc.refund_tx_id IS NULL AND p.stage IN ('CANCELED', 'FAILED')"
|
'''
|
||||||
|
pc.status = 'CONFIRMED' AND
|
||||||
|
pc.staking = FALSE AND
|
||||||
|
pc.refund_tx_id IS NULL AND
|
||||||
|
p.stage IN ('CANCELED', 'FAILED') AND
|
||||||
|
us.refund_address IS NOT NULL
|
||||||
|
'''
|
||||||
))),
|
))),
|
||||||
|
# here we don't care about current refund_address of user, just that there has been a refund_tx_id
|
||||||
'refunded': str(ex(sql_pc_p(
|
'refunded': str(ex(sql_pc_p(
|
||||||
"pc.status = 'CONFIRMED' AND pc.staking = FALSE AND pc.no_refund = FALSE AND pc.refund_tx_id IS NOT NULL AND p.stage IN ('CANCELED', 'FAILED')"
|
'''
|
||||||
|
pc.status = 'CONFIRMED' AND
|
||||||
|
pc.staking = FALSE AND
|
||||||
|
pc.refund_tx_id IS NOT NULL AND
|
||||||
|
p.stage IN ('CANCELED', 'FAILED')
|
||||||
|
'''
|
||||||
))),
|
))),
|
||||||
|
# if there is no user, or the user hasn't any refund_address
|
||||||
'donations': str(ex(sql_pc_p(
|
'donations': str(ex(sql_pc_p(
|
||||||
"(pc.status = 'CONFIRMED' AND pc.staking = FALSE AND pc.refund_tx_id IS NULL) AND (pc.no_refund = TRUE OR pc.user_id IS NULL) AND p.stage IN ('CANCELED', 'FAILED')"
|
'''
|
||||||
|
pc.status = 'CONFIRMED' AND
|
||||||
|
pc.staking = FALSE AND
|
||||||
|
pc.refund_tx_id IS NULL AND
|
||||||
|
(pc.user_id IS NULL OR us.refund_address IS NULL) AND
|
||||||
|
p.stage IN ('CANCELED', 'FAILED')
|
||||||
|
'''
|
||||||
))),
|
))),
|
||||||
'gross': str(ex(sql_pc_p("pc.status = 'CONFIRMED' AND pc.refund_tx_id IS NULL"))),
|
'gross': str(ex(sql_pc_p("pc.status = 'CONFIRMED' AND pc.refund_tx_id IS NULL"))),
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ class Comment(db.Model):
|
||||||
self.proposal_id = proposal_id
|
self.proposal_id = proposal_id
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.parent_comment_id = parent_comment_id
|
self.parent_comment_id = parent_comment_id
|
||||||
self.content = content[:1000]
|
self.content = content[:5000]
|
||||||
self.date_created = datetime.datetime.now()
|
self.date_created = datetime.datetime.now()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -88,7 +88,7 @@ class ProposalContribution(db.Model):
|
||||||
tx_id = db.Column(db.String(255), nullable=True)
|
tx_id = db.Column(db.String(255), nullable=True)
|
||||||
refund_tx_id = db.Column(db.String(255), nullable=True)
|
refund_tx_id = db.Column(db.String(255), nullable=True)
|
||||||
staking = db.Column(db.Boolean, nullable=False)
|
staking = db.Column(db.Boolean, nullable=False)
|
||||||
no_refund = db.Column(db.Boolean, nullable=False)
|
private = db.Column(db.Boolean, nullable=False, default=False, server_default='true')
|
||||||
|
|
||||||
user = db.relationship("User")
|
user = db.relationship("User")
|
||||||
|
|
||||||
|
@ -98,23 +98,23 @@ class ProposalContribution(db.Model):
|
||||||
amount: str,
|
amount: str,
|
||||||
user_id: int = None,
|
user_id: int = None,
|
||||||
staking: bool = False,
|
staking: bool = False,
|
||||||
no_refund: bool = False,
|
private: bool = True,
|
||||||
):
|
):
|
||||||
self.proposal_id = proposal_id
|
self.proposal_id = proposal_id
|
||||||
self.amount = amount
|
self.amount = amount
|
||||||
self.user_id = user_id
|
self.user_id = user_id
|
||||||
self.staking = staking
|
self.staking = staking
|
||||||
self.no_refund = no_refund
|
self.private = private
|
||||||
self.date_created = datetime.datetime.now()
|
self.date_created = datetime.datetime.now()
|
||||||
self.status = ContributionStatus.PENDING
|
self.status = ContributionStatus.PENDING
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_existing_contribution(user_id: int, proposal_id: int, amount: str, no_refund: bool = False):
|
def get_existing_contribution(user_id: int, proposal_id: int, amount: str, private: bool = False):
|
||||||
return ProposalContribution.query.filter_by(
|
return ProposalContribution.query.filter_by(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
proposal_id=proposal_id,
|
proposal_id=proposal_id,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
no_refund=no_refund,
|
private=private,
|
||||||
status=ContributionStatus.PENDING,
|
status=ContributionStatus.PENDING,
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
@ -425,14 +425,14 @@ class Proposal(db.Model):
|
||||||
amount,
|
amount,
|
||||||
user_id: int = None,
|
user_id: int = None,
|
||||||
staking: bool = False,
|
staking: bool = False,
|
||||||
no_refund: bool = False,
|
private: bool = True,
|
||||||
):
|
):
|
||||||
contribution = ProposalContribution(
|
contribution = ProposalContribution(
|
||||||
proposal_id=self.id,
|
proposal_id=self.id,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
staking=staking,
|
staking=staking,
|
||||||
no_refund=no_refund,
|
private=private
|
||||||
)
|
)
|
||||||
db.session.add(contribution)
|
db.session.add(contribution)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
@ -841,6 +841,7 @@ class ProposalContributionSchema(ma.Schema):
|
||||||
"date_created",
|
"date_created",
|
||||||
"addresses",
|
"addresses",
|
||||||
"is_anonymous",
|
"is_anonymous",
|
||||||
|
"private"
|
||||||
)
|
)
|
||||||
|
|
||||||
proposal = ma.Nested("ProposalSchema")
|
proposal = ma.Nested("ProposalSchema")
|
||||||
|
@ -861,11 +862,11 @@ class ProposalContributionSchema(ma.Schema):
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_is_anonymous(self, obj):
|
def get_is_anonymous(self, obj):
|
||||||
return not obj.user_id
|
return not obj.user_id or obj.private
|
||||||
|
|
||||||
@post_dump
|
@post_dump
|
||||||
def stub_anonymous_user(self, data):
|
def stub_anonymous_user(self, data):
|
||||||
if 'user' in data and data['user'] is None:
|
if 'user' in data and data['user'] is None or data['private']:
|
||||||
data['user'] = anonymous_user
|
data['user'] = anonymous_user
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -894,7 +895,7 @@ class AdminProposalContributionSchema(ma.Schema):
|
||||||
"refund_address",
|
"refund_address",
|
||||||
"refund_tx_id",
|
"refund_tx_id",
|
||||||
"staking",
|
"staking",
|
||||||
"no_refund",
|
"private",
|
||||||
)
|
)
|
||||||
|
|
||||||
proposal = ma.Nested("ProposalSchema")
|
proposal = ma.Nested("ProposalSchema")
|
||||||
|
|
|
@ -99,7 +99,7 @@ def report_proposal_comment(proposal_id, comment_id):
|
||||||
@limiter.limit("30/hour;2/minute")
|
@limiter.limit("30/hour;2/minute")
|
||||||
@requires_email_verified_auth
|
@requires_email_verified_auth
|
||||||
@body({
|
@body({
|
||||||
"comment": fields.Str(required=True, validate=validate.Length(max=1000)),
|
"comment": fields.Str(required=True, validate=validate.Length(max=5000)),
|
||||||
"parentCommentId": fields.Int(required=False, missing=None),
|
"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):
|
||||||
|
@ -478,30 +478,26 @@ def get_proposal_contribution(proposal_id, contribution_id):
|
||||||
@limiter.limit("30/day;10/hour;2/minute")
|
@limiter.limit("30/day;10/hour;2/minute")
|
||||||
@body({
|
@body({
|
||||||
"amount": fields.Str(required=True, validate=lambda p: 0.0001 <= float(p) <= 1000000),
|
"amount": fields.Str(required=True, validate=lambda p: 0.0001 <= float(p) <= 1000000),
|
||||||
"anonymous": fields.Bool(required=False, missing=None),
|
"private": fields.Bool(required=False, missing=True)
|
||||||
"noRefund": fields.Bool(required=False, missing=False),
|
|
||||||
})
|
})
|
||||||
def post_proposal_contribution(proposal_id, amount, anonymous, no_refund):
|
def post_proposal_contribution(proposal_id, amount, private):
|
||||||
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
||||||
if not proposal:
|
if not proposal:
|
||||||
return {"message": "No proposal matching id"}, 404
|
return {"message": "No proposal matching id"}, 404
|
||||||
|
|
||||||
code = 200
|
code = 200
|
||||||
user = None
|
|
||||||
contribution = None
|
|
||||||
|
|
||||||
if not anonymous:
|
|
||||||
user = get_authed_user()
|
user = get_authed_user()
|
||||||
|
contribution = None
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
contribution = ProposalContribution \
|
contribution = ProposalContribution \
|
||||||
.get_existing_contribution(user.id, proposal_id, amount, no_refund)
|
.get_existing_contribution(user.id, proposal_id, amount, private)
|
||||||
|
|
||||||
if not contribution:
|
if not contribution:
|
||||||
code = 201
|
code = 201
|
||||||
contribution = proposal.create_contribution(
|
contribution = proposal.create_contribution(
|
||||||
amount=amount,
|
amount=amount,
|
||||||
no_refund=no_refund,
|
private=private,
|
||||||
user_id=user.id if user else None,
|
user_id=user.id if user else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -66,6 +66,7 @@ def get_user(user_id, with_proposals, with_comments, with_funded, with_pending,
|
||||||
contributions = ProposalContribution.get_by_userid(user_id)
|
contributions = ProposalContribution.get_by_userid(user_id)
|
||||||
if not authed_user or user.id != authed_user.id:
|
if not authed_user or user.id != authed_user.id:
|
||||||
contributions = [c for c in contributions if c.status == ContributionStatus.CONFIRMED]
|
contributions = [c for c in contributions if c.status == ContributionStatus.CONFIRMED]
|
||||||
|
contributions = [c for c in contributions if not c.private]
|
||||||
contributions = [c for c in contributions if c.proposal.status == ProposalStatus.LIVE]
|
contributions = [c for c in contributions if c.proposal.status == ProposalStatus.LIVE]
|
||||||
contributions_dump = user_proposal_contributions_schema.dump(contributions)
|
contributions_dump = user_proposal_contributions_schema.dump(contributions)
|
||||||
result["contributions"] = contributions_dump
|
result["contributions"] = contributions_dump
|
||||||
|
|
|
@ -159,7 +159,6 @@ class ContributionPagination(Pagination):
|
||||||
if 'REFUNDABLE' in filters:
|
if 'REFUNDABLE' in filters:
|
||||||
query = query.filter(ProposalContribution.refund_tx_id == None) \
|
query = query.filter(ProposalContribution.refund_tx_id == None) \
|
||||||
.filter(ProposalContribution.staking == False) \
|
.filter(ProposalContribution.staking == False) \
|
||||||
.filter(ProposalContribution.no_refund == False) \
|
|
||||||
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
|
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
|
||||||
.join(Proposal) \
|
.join(Proposal) \
|
||||||
.filter(or_(
|
.filter(or_(
|
||||||
|
@ -173,15 +172,14 @@ class ContributionPagination(Pagination):
|
||||||
if 'DONATION' in filters:
|
if 'DONATION' in filters:
|
||||||
query = query.filter(ProposalContribution.refund_tx_id == None) \
|
query = query.filter(ProposalContribution.refund_tx_id == None) \
|
||||||
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
|
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
|
||||||
.filter(or_(
|
|
||||||
ProposalContribution.no_refund == True,
|
|
||||||
ProposalContribution.user_id == None,
|
|
||||||
)) \
|
|
||||||
.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, isouter=True) \
|
||||||
|
.join(UserSettings, isouter=True) \
|
||||||
|
.filter(UserSettings.refund_address == None)
|
||||||
|
|
||||||
# SORT (see self.SORT_MAP)
|
# SORT (see self.SORT_MAP)
|
||||||
if sort:
|
if sort:
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
"""proposal_contribution: add private, remove no_refund
|
||||||
|
|
||||||
|
Revision ID: 4505f00c4ebd
|
||||||
|
Revises: 0f08974b4118
|
||||||
|
Create Date: 2019-06-07 10:31:47.120185
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '4505f00c4ebd'
|
||||||
|
down_revision = '0f08974b4118'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('proposal_contribution', sa.Column('private', sa.Boolean(), server_default='true', nullable=False))
|
||||||
|
op.drop_column('proposal_contribution', 'no_refund')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
# existing contributions with user ids are public
|
||||||
|
op.execute("UPDATE proposal_contribution SET private = FALSE WHERE user_id IS NOT NULL")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('proposal_contribution', sa.Column('no_refund', sa.BOOLEAN(), autoincrement=False, nullable=False))
|
||||||
|
op.drop_column('proposal_contribution', 'private')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -1,4 +1,5 @@
|
||||||
from grant.utils.enums import Category
|
from grant.utils.enums import Category
|
||||||
|
|
||||||
from .mocks import mock_request
|
from .mocks import mock_request
|
||||||
|
|
||||||
test_user = {
|
test_user = {
|
||||||
|
@ -54,21 +55,70 @@ test_comment = {
|
||||||
|
|
||||||
test_comment_large = {
|
test_comment_large = {
|
||||||
"comment": """
|
"comment": """
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
massa vitae tortor condimentum lacinia quis vel eros donec ac odio tempor orci dapibus ultrices
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
in iaculis nunc sed augue lacus viverra vitae congue eu consequat ac felis donec et odio
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
faucibus scelerisque eleifend donec pretium vulputate sapien nec
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
sagittis aliquam malesuada bibendum arcu vitae elementum curabitur
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
vitae nunc sed velit dignissim sodales ut eu sem integer vitae justo
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
eget magna fermentum iaculis eu non diam phasellus vestibulum lorem sed
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
risus ultricies tristique nulla aliquet enim tortor at auctor urna nunc id
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
cursus metus aliquam eleifend mi in nulla posuere sollicitudin aliquam ultrices
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
sagittis orci a scelerisque purus semper eget duis at tellus at urna condimentum
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
mattis pellentesque id nibh tortor id aliquet lectus proin nibh nisl condimentum id
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
venenatis a condimentum vitae sapien pellentesque habitant morbi tristique senectus
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
et netus et malesuada fames ac turpis egestas sed tempus urna et pharetra pharetra
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
massa massa ultricies mi quis hendrerit dolor magna eget est lorem ipsum dolor sit
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
amet consectetur adipiscing elit pellentesque habitant morbi tristique senectus et
|
||||||
|
netus et malesuada fames ac turpis egestas integer eget aliquet nibh praesent
|
||||||
|
tristique magna sit amet purus gravida quis blandit turpis cursus in hac habitasse
|
||||||
|
platea dictumst quisque sagittis purus sit amet volutpat consequat mauris nunc
|
||||||
|
congue nisi vitae suscipit tellus mauris a diam maecenas sed enim ut sem viverra
|
||||||
|
aliquet eget sit amet tellus cras adipiscing enim eu turpis egestas pretium aenean
|
||||||
|
pharetra magna ac placerat vestibulum lectus mauris ultrices eros in cursus
|
||||||
|
turpis massa tincidunt dui ut ornare lectus sit amet est placerat in egestas
|
||||||
|
erat imperdiet sed euismod nisi porta lorem mollis aliquam ut
|
||||||
|
porttitor leo a diam sollicitudin tempor id eu nisl nunc mi
|
||||||
|
ipsum faucibus vitae aliquet nec ullamcorper sit amet risus
|
||||||
|
nullam eget felis eget nunc lobortis mattis aliquam faucibus
|
||||||
|
purus in massa tempor nec feugiat nisl pretium fusce id velit ut tortor
|
||||||
|
pretium viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare suspendisse
|
||||||
|
sed nisi lacus sed viverra tellus in hac habitasse platea
|
||||||
|
dictumst vestibulum rhoncus est pellentesque
|
||||||
|
elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus at augue eget arcu dictum varius duis at
|
||||||
|
consectetur lorem donec massa sapien faucibus et molestie ac feugiat sed lectus
|
||||||
|
vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare massa
|
||||||
|
eget egestas purus viverra accumsan in nisl nisi scelerisque eu ultrices vitae auctor
|
||||||
|
eu augue ut lectus arcu bibendum at varius vel pharetra vel turpis nunc eget lorem dolor sed viverra ipsum nunc
|
||||||
|
aliquet bibendum enim facilisis gravida neque convallis a cras semper auctor neque vitae tempus quam pellentesque
|
||||||
|
nec nam aliquam sem et tortor consequat id porta nibh venenatis cras sed felis eget velit aliquet sagittis id
|
||||||
|
consectetur purus ut faucibus pulvinar elementum integer enim neque volutpat ac tincidunt vitae semper quis lectus
|
||||||
|
nulla at volutpat diam ut venenatis tellus in metus vulputate eu scelerisque felis imperdiet proin fermentum leo
|
||||||
|
vel orci porta non pulvinar neque laoreet suspendisse interdum consectetur libero id faucibus nisl tincidunt eget
|
||||||
|
nullam non nisi est sit amet facilisis magna etiam tempor orci eu lobortis elementum nibh tellus molestie nunc non
|
||||||
|
blandit massa enim nec dui nunc mattis enim ut tellus elementum sagittis vitae et leo duis ut diam quam nulla
|
||||||
|
porttitor massa id neque aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus imperdiet nulla
|
||||||
|
malesuada pellentesque elit eget gravida cum sociis natoque penatibus et magnis dis parturient montes nascetur
|
||||||
|
ridiculus mus mauris vitae ultricies leo integer malesuada nunc vel risus commodo viverra maecenas accumsan lacus
|
||||||
|
vel facilisis volutpat est velit egestas dui id ornare arcu odio ut sem nulla pharetra diam sit amet nisl suscipit
|
||||||
|
adipiscing bibendum est ultricies integer quis auctor elit sed vulputate mi sit amet mauris commodo quis imperdiet
|
||||||
|
massa tincidunt nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum varius
|
||||||
|
sit amet mattis vulputate enim nulla aliquet porttitor lacus luctus accumsan tortor posuere ac ut consequat semper
|
||||||
|
viverra nam libero justo laoreet sit amet cursus sit amet dictum sit amet justo donec enim diam vulputate ut
|
||||||
|
pharetra sit amet aliquam id diam maecenas ultricies mi eget mauris pharetra et ultrices neque ornare aenean
|
||||||
|
euismod elementum nisi quis eleifend quam adipiscing vitae proin sagittis nisl rhoncus mattis rhoncus urna neque
|
||||||
|
viverra justo nec ultrices dui sapien eget mi proin sed libero enim sed faucibus turpis in eu mi bibendum neque
|
||||||
|
egestas congue quisque egestas diam in arcu cursus euismod quis viverra nibh cras pulvinar mattis nunc sed blandit
|
||||||
|
libero volutpat sed cras ornare arcu dui vivamus arcu felis bibendum ut tristique et egestas quis ipsum
|
||||||
|
suspendisse ultrices gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim sit amet venenatis
|
||||||
|
urna cursus eget nunc scelerisque viverra mauris in aliquam sem fringilla ut morbi tincidunt augue interdum velit
|
||||||
|
euismod in pellentesque massa placerat duis ultricies lacus sed turpis tincidunt id aliquet risus feugiat in ante
|
||||||
|
metus dictum at tempor commodo ullamcorper a lacus vestibulum sed arcu non odio euismod lacinia at quis risus sed
|
||||||
|
vulputate odio ut enim blandit volutpat maecenas volutpat blandit aliquam etiam erat velit scelerisque in dictum
|
||||||
|
non consectetur a erat nam at lectus urna duis convallis convallis tellus id interdum velit laoreet id donec
|
||||||
|
ultrices tincidunt arcu non sodales neque
|
||||||
|
sodales ut etiam sit amet nisl purus in mollis nunc sed id semper risus in hendrerit gravida rutrum quisque non
|
||||||
|
tellus orci ac auctor augue mauris augue neque gravida in fermentum et sollicitudin
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -316,13 +316,11 @@ export function putInviteResponse(
|
||||||
export function postProposalContribution(
|
export function postProposalContribution(
|
||||||
proposalId: number,
|
proposalId: number,
|
||||||
amount: string,
|
amount: string,
|
||||||
anonymous: boolean = false,
|
isPrivate: boolean = true,
|
||||||
noRefund: boolean = false,
|
|
||||||
): Promise<{ data: ContributionWithAddressesAndUser }> {
|
): Promise<{ data: ContributionWithAddressesAndUser }> {
|
||||||
return axios.post(`/api/v1/proposals/${proposalId}/contributions`, {
|
return axios.post(`/api/v1/proposals/${proposalId}/contributions`, {
|
||||||
amount,
|
amount,
|
||||||
anonymous,
|
private: isPrivate,
|
||||||
noRefund,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ interface OwnProps {
|
||||||
contributionId?: number;
|
contributionId?: number;
|
||||||
amount?: string;
|
amount?: string;
|
||||||
isAnonymous?: boolean;
|
isAnonymous?: boolean;
|
||||||
|
isPublic?: boolean;
|
||||||
hasNoButtons?: boolean;
|
hasNoButtons?: boolean;
|
||||||
text?: React.ReactNode;
|
text?: React.ReactNode;
|
||||||
handleClose(): void;
|
handleClose(): void;
|
||||||
|
@ -127,17 +128,13 @@ class ContributionModal extends React.Component<Props, State> {
|
||||||
message="This contribution will not be attributed"
|
message="This contribution will not be attributed"
|
||||||
description={
|
description={
|
||||||
<>
|
<>
|
||||||
Your contribution will show up without attribution. Even if you're logged
|
ZF Grants is unable to offer refunds for contributions made without
|
||||||
in, the contribution will not appear anywhere on your account after you
|
accounts with a set refund address. If refunds for this campaign are issued, your contribution will be
|
||||||
close this modal.
|
treated as a donation to the Zcash Foundation.
|
||||||
<br /> <br />
|
<br /> <br />
|
||||||
ZF Grants is unable to offer refunds for non-attributed contributions. If
|
If you would like your contribution to be eligible for refund, you can close
|
||||||
refunds for this campaign are issued, your contribution will be treated as a
|
this modal, make sure you're logged in, set a refund address, and attempt to contribute again. You
|
||||||
donation to the Zcash Foundation.
|
can still choose to contribute without public attribution while logged in.
|
||||||
<br /> <br />
|
|
||||||
If you would like to have your contribution attached to an account and
|
|
||||||
remain eligible for refunds, you can close this modal, make sure you're
|
|
||||||
logged in, and don't check the "Contribute without attribution" checkbox.
|
|
||||||
<br /> <br />
|
<br /> <br />
|
||||||
NOTE: The Zcash Foundation is unable to accept donations of more than $5,000
|
NOTE: The Zcash Foundation is unable to accept donations of more than $5,000
|
||||||
USD worth of ZEC from anonymous users.
|
USD worth of ZEC from anonymous users.
|
||||||
|
@ -221,7 +218,7 @@ class ContributionModal extends React.Component<Props, State> {
|
||||||
) {
|
) {
|
||||||
this.setState({ isFetchingContribution: true });
|
this.setState({ isFetchingContribution: true });
|
||||||
try {
|
try {
|
||||||
const { amount, isAnonymous, authUser } = this.props;
|
const { amount, isAnonymous, authUser, isPublic } = this.props;
|
||||||
|
|
||||||
// Ensure auth'd users have a refund address unless they've confirmed
|
// Ensure auth'd users have a refund address unless they've confirmed
|
||||||
if (!isAnonymous && !noRefund) {
|
if (!isAnonymous && !noRefund) {
|
||||||
|
@ -240,12 +237,7 @@ class ContributionModal extends React.Component<Props, State> {
|
||||||
if (contributionId) {
|
if (contributionId) {
|
||||||
res = await getProposalContribution(proposalId, contributionId);
|
res = await getProposalContribution(proposalId, contributionId);
|
||||||
} else {
|
} else {
|
||||||
res = await postProposalContribution(
|
res = await postProposalContribution(proposalId, amount || '0', !isPublic);
|
||||||
proposalId,
|
|
||||||
amount || '0',
|
|
||||||
isAnonymous,
|
|
||||||
noRefund,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
this.setState({ contribution: res.data });
|
this.setState({ contribution: res.data });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -39,10 +39,7 @@ const commands: { [key in MARKDOWN_TYPE]: ReactMdeProps['commands'] } = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
commands: [
|
commands: [ReactMdeCommands.linkCommand, ReactMdeCommands.quoteCommand],
|
||||||
ReactMdeCommands.linkCommand,
|
|
||||||
ReactMdeCommands.quoteCommand,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
commands: [
|
commands: [
|
||||||
|
@ -104,7 +101,7 @@ export default class MarkdownEditor extends React.PureComponent<Props, State> {
|
||||||
const { randomKey, value, tab } = this.state;
|
const { randomKey, value, tab } = this.state;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={(el) => this.el = el}
|
ref={el => (this.el = el)}
|
||||||
className={classnames({
|
className={classnames({
|
||||||
MarkdownEditor: true,
|
MarkdownEditor: true,
|
||||||
['is-reduced']: type === MARKDOWN_TYPE.REDUCED,
|
['is-reduced']: type === MARKDOWN_TYPE.REDUCED,
|
||||||
|
@ -119,6 +116,7 @@ export default class MarkdownEditor extends React.PureComponent<Props, State> {
|
||||||
generateMarkdownPreview={this.generatePreview}
|
generateMarkdownPreview={this.generatePreview}
|
||||||
commands={commands[type]}
|
commands={commands[type]}
|
||||||
readOnly={!!readOnly}
|
readOnly={!!readOnly}
|
||||||
|
textAreaProps={{ maxLength: 5000 }}
|
||||||
minEditorHeight={minHeight}
|
minEditorHeight={minHeight}
|
||||||
minPreviewHeight={minHeight - 10}
|
minPreviewHeight={minHeight - 10}
|
||||||
maxEditorHeight={99999}
|
maxEditorHeight={99999}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Tag, Popconfirm } from 'antd';
|
import { Tag, Popconfirm, Tooltip } from 'antd';
|
||||||
import UnitDisplay from 'components/UnitDisplay';
|
import UnitDisplay from 'components/UnitDisplay';
|
||||||
import { ONE_DAY } from 'utils/time';
|
import { ONE_DAY } from 'utils/time';
|
||||||
import { formatTxExplorerUrl } from 'utils/formatters';
|
import { formatTxExplorerUrl } from 'utils/formatters';
|
||||||
|
@ -25,7 +25,7 @@ type Props = OwnProps & DispatchProps;
|
||||||
class ProfileContribution extends React.Component<Props> {
|
class ProfileContribution extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { contribution } = this.props;
|
const { contribution } = this.props;
|
||||||
const { proposal } = contribution;
|
const { proposal, private: isPrivate } = contribution;
|
||||||
const isConfirmed = contribution.status === 'CONFIRMED';
|
const isConfirmed = contribution.status === 'CONFIRMED';
|
||||||
const isExpired =
|
const isExpired =
|
||||||
(!isConfirmed && contribution.dateCreated < Date.now() / 1000 - ONE_DAY) ||
|
(!isConfirmed && contribution.dateCreated < Date.now() / 1000 - ONE_DAY) ||
|
||||||
|
@ -63,6 +63,18 @@ class ProfileContribution extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const privateTag = isPrivate ? (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Other users will <b>not</b> be able to see that you made this contribution.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Tag>Private</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ProfileContribution">
|
<div className="ProfileContribution">
|
||||||
<div className="ProfileContribution-info">
|
<div className="ProfileContribution-info">
|
||||||
|
@ -70,7 +82,8 @@ class ProfileContribution extends React.Component<Props> {
|
||||||
className="ProfileContribution-info-title"
|
className="ProfileContribution-info-title"
|
||||||
to={`/proposals/${proposal.proposalId}`}
|
to={`/proposals/${proposal.proposalId}`}
|
||||||
>
|
>
|
||||||
{proposal.title} {tag}
|
{proposal.title} {privateTag}
|
||||||
|
{tag}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="ProfileContribution-info-brief">{proposal.brief}</div>
|
<div className="ProfileContribution-info-brief">{proposal.brief}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Form, Input, Checkbox, Button, Icon, Popover, Tooltip } from 'antd';
|
import { Form, Input, Button, Icon, Popover, Tooltip, Radio } from 'antd';
|
||||||
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
|
import { RadioChangeEvent } from 'antd/lib/radio';
|
||||||
import { Proposal, STATUS } from 'types';
|
import { Proposal, STATUS } from 'types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { fromZat } from 'utils/units';
|
import { fromZat } from 'utils/units';
|
||||||
|
@ -30,7 +30,7 @@ type Props = OwnProps & StateProps;
|
||||||
interface State {
|
interface State {
|
||||||
amountToRaise: string;
|
amountToRaise: string;
|
||||||
amountError: string | null;
|
amountError: string | null;
|
||||||
isAnonymous: boolean;
|
isPrivate: boolean;
|
||||||
isContributing: boolean;
|
isContributing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,14 +40,14 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
this.state = {
|
this.state = {
|
||||||
amountToRaise: '',
|
amountToRaise: '',
|
||||||
amountError: null,
|
amountError: null,
|
||||||
isAnonymous: false,
|
isPrivate: true,
|
||||||
isContributing: false,
|
isContributing: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { proposal, isPreview, authUser } = this.props;
|
const { proposal, isPreview, authUser } = this.props;
|
||||||
const { amountToRaise, amountError, isAnonymous, isContributing } = this.state;
|
const { amountToRaise, amountError, isPrivate, isContributing } = this.state;
|
||||||
const amountFloat = parseFloat(amountToRaise) || 0;
|
const amountFloat = parseFloat(amountToRaise) || 0;
|
||||||
let content;
|
let content;
|
||||||
if (proposal) {
|
if (proposal) {
|
||||||
|
@ -190,12 +190,23 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{amountToRaise &&
|
{amountToRaise &&
|
||||||
!!authUser && (
|
!!authUser && (
|
||||||
<Checkbox checked={isAnonymous} onChange={this.handleChangeAnonymity}>
|
<Radio.Group
|
||||||
|
onChange={this.handleChangePrivate}
|
||||||
|
value={isPrivate ? 'isPrivate' : 'isNotPrivate'}
|
||||||
|
>
|
||||||
|
<Radio value={'isPrivate'}>
|
||||||
Contribute without attribution
|
Contribute without attribution
|
||||||
<Tooltip title="Your contribution will not be linked to your account. ZF Grants cannot refund non-attributed contributions.">
|
<Tooltip title="Other users will not see who made this contribution.">
|
||||||
<Icon type="question-circle" />
|
<Icon type="question-circle" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Checkbox>
|
</Radio>
|
||||||
|
<Radio value={'isNotPrivate'}>
|
||||||
|
Attribute contribution publicly
|
||||||
|
<Tooltip title="Other users will be able to see that you made this contribution.">
|
||||||
|
<Icon type="question-circle" />
|
||||||
|
</Tooltip>
|
||||||
|
</Radio>
|
||||||
|
</Radio.Group>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={this.openContributionModal}
|
onClick={this.openContributionModal}
|
||||||
|
@ -214,7 +225,8 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
isVisible={isContributing}
|
isVisible={isContributing}
|
||||||
proposalId={proposal.proposalId}
|
proposalId={proposal.proposalId}
|
||||||
amount={amountToRaise}
|
amount={amountToRaise}
|
||||||
isAnonymous={isAnonymous || !authUser}
|
isAnonymous={!authUser}
|
||||||
|
isPublic={!isPrivate}
|
||||||
handleClose={this.closeContributionModal}
|
handleClose={this.closeContributionModal}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -255,8 +267,9 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
||||||
this.setState({ amountToRaise: value, amountError });
|
this.setState({ amountToRaise: value, amountError });
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleChangeAnonymity = (ev: CheckboxChangeEvent) => {
|
private handleChangePrivate = (ev: RadioChangeEvent) => {
|
||||||
this.setState({ isAnonymous: ev.target.checked });
|
const isPrivate = ev.target.value === 'isPrivate';
|
||||||
|
this.setState({ isPrivate });
|
||||||
};
|
};
|
||||||
|
|
||||||
private openContributionModal = () => this.setState({ isContributing: true });
|
private openContributionModal = () => this.setState({ isContributing: true });
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
margin: 0.5rem -1.5rem;
|
margin: 0.5rem -1.5rem;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #FFF;
|
color: #fff;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
||||||
.anticon {
|
.anticon {
|
||||||
|
@ -57,7 +57,6 @@
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
&-popover {
|
&-popover {
|
||||||
&-overlay {
|
&-overlay {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
|
@ -91,7 +90,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-checkbox-wrapper {
|
.ant-radio-wrapper {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
LoadableProposalPage,
|
LoadableProposalPage,
|
||||||
Moreable,
|
Moreable,
|
||||||
} from 'types';
|
} from 'types';
|
||||||
import { PROPOSAL_SORT, PROPOSAL_STAGE } from 'api/constants';
|
import { PROPOSAL_SORT } from 'api/constants';
|
||||||
|
|
||||||
export interface ProposalDetail extends Proposal {
|
export interface ProposalDetail extends Proposal {
|
||||||
isRequestingPayout: boolean;
|
isRequestingPayout: boolean;
|
||||||
|
@ -62,7 +62,7 @@ export const INITIAL_STATE: ProposalState = {
|
||||||
sort: PROPOSAL_SORT.NEWEST,
|
sort: PROPOSAL_SORT.NEWEST,
|
||||||
filters: {
|
filters: {
|
||||||
category: [],
|
category: [],
|
||||||
stage: [PROPOSAL_STAGE.FUNDING_REQUIRED],
|
stage: [],
|
||||||
},
|
},
|
||||||
items: [],
|
items: [],
|
||||||
hasFetched: false,
|
hasFetched: false,
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
* You may reach out to the Zcash Foundation by emailing us at contact@zfnd.org
|
* You may reach out to the Zcash Foundation by emailing us at contact@zfnd.org
|
||||||
* You can find us on twitter at https://twitter.com/zcashfoundation
|
* You can find us on twitter at https://twitter.com/zcashfoundation
|
||||||
* You can contribute or report issues at https://github.com/zcashfoundation
|
* You can contribute or report issues at https://github.com/ZcashFoundation/zcash-grant-system/issues
|
||||||
|
|
|
@ -30,4 +30,5 @@ export interface UserContribution extends Omit<Contribution, 'amount' | 'txId'>
|
||||||
amount: Zat;
|
amount: Zat;
|
||||||
txId?: string;
|
txId?: string;
|
||||||
proposal: Proposal;
|
proposal: Proposal;
|
||||||
|
private: boolean;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue