commit
dd966ae853
|
@ -89,7 +89,7 @@
|
|||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-dev-utils": "^5.0.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-router": "^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) => (
|
||||
<div className="ContributionDetail-deet">
|
||||
<div className="ContributionDetail-deet-value">
|
||||
{val}
|
||||
</div>
|
||||
<div className="ContributionDetail-deet-label">
|
||||
{label}
|
||||
</div>
|
||||
<div className="ContributionDetail-deet-value">{val}</div>
|
||||
<div className="ContributionDetail-deet-label">{label}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
const percent = c.proposal.milestones.reduce((prev, m) => {
|
||||
return m.datePaid ? prev - parseFloat(m.payoutPercent) : prev;
|
||||
}, 100);
|
||||
const amount = toZat(c.amount).muln(percent).divn(100);
|
||||
const amount = toZat(c.amount)
|
||||
.muln(percent)
|
||||
.divn(100);
|
||||
return (
|
||||
<Alert
|
||||
className="ContributionDetail-alert"
|
||||
|
@ -62,15 +66,15 @@ class ContributionDetail extends React.Component<Props, State> {
|
|||
description={
|
||||
<div>
|
||||
<p>
|
||||
The proposal this contribution was made towards has failed, and
|
||||
is ready to be refunded to <strong>{c.user.displayName}</strong>
|
||||
{' '}for <strong>{percent}%</strong> of the contribution amount.
|
||||
Please Make a payment of <strong>{fromZat(amount)} ZEC</strong> to:
|
||||
The proposal this contribution was made towards has failed, and is ready
|
||||
to be refunded to <strong>{c.user.displayName}</strong> for{' '}
|
||||
<strong>{percent}%</strong> of the contribution amount. Please Make a
|
||||
payment of <strong>{fromZat(amount)} ZEC</strong> to:
|
||||
</p>
|
||||
<pre>{c.refundAddress}</pre>
|
||||
<p>
|
||||
They will be sent an email notifying them of the refund when you
|
||||
enter the txid below.
|
||||
They will be sent an email notifying them of the refund when you enter the
|
||||
txid below.
|
||||
</p>
|
||||
<Input.Search
|
||||
placeholder="Enter payment txid"
|
||||
|
@ -98,10 +102,9 @@ class ContributionDetail extends React.Component<Props, State> {
|
|||
<pre>{JSON.stringify(c.addresses, null, 4)}</pre>
|
||||
</Collapse.Panel>
|
||||
|
||||
|
||||
<Collapse.Panel key="user" header="user">
|
||||
{c.user ? <UserItem {...c.user} /> : <em>Anonymous contribution</em>}
|
||||
</Collapse.Panel>
|
||||
<Collapse.Panel key="user" header="user">
|
||||
{c.user ? <UserItem {...c.user} /> : <em>Anonymous contribution</em>}
|
||||
</Collapse.Panel>
|
||||
|
||||
<Collapse.Panel key="proposal" header="proposal">
|
||||
<ProposalItem {...c.proposal} />
|
||||
|
@ -118,7 +121,9 @@ class ContributionDetail extends React.Component<Props, State> {
|
|||
{/* ACTIONS */}
|
||||
<Card size="small" className="ContributionDetail-controls">
|
||||
<Link to={`/contributions/${id}/edit`}>
|
||||
<Button type="primary" block>Edit</Button>
|
||||
<Button type="primary" block>
|
||||
Edit
|
||||
</Button>
|
||||
</Link>
|
||||
</Card>
|
||||
|
||||
|
@ -128,16 +133,20 @@ class ContributionDetail extends React.Component<Props, State> {
|
|||
{renderDeetItem('created', formatDateSeconds(c.dateCreated))}
|
||||
{renderDeetItem('status', c.status)}
|
||||
{renderDeetItem('amount', c.amount)}
|
||||
{renderDeetItem('txid', c.txId
|
||||
? <Input size="small" value={c.txId} readOnly />
|
||||
: <em>N/A</em>
|
||||
{renderDeetItem(
|
||||
'txid',
|
||||
c.txId ? <Input size="small" value={c.txId} readOnly /> : <em>N/A</em>,
|
||||
)}
|
||||
{renderDeetItem('refund txid', c.refundTxId
|
||||
? <Input size="small" value={c.refundTxId} readOnly />
|
||||
: <em>N/A</em>
|
||||
{renderDeetItem(
|
||||
'refund txid',
|
||||
c.refundTxId ? (
|
||||
<Input size="small" value={c.refundTxId} readOnly />
|
||||
) : (
|
||||
<em>N/A</em>
|
||||
),
|
||||
)}
|
||||
{renderDeetItem('staking tx', JSON.stringify(c.staking))}
|
||||
{renderDeetItem('no refund', JSON.stringify(c.noRefund))}
|
||||
{renderDeetItem('private', JSON.stringify(c.private))}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -152,7 +152,7 @@ export interface Contribution {
|
|||
memo: string;
|
||||
};
|
||||
staking: boolean;
|
||||
noRefund: boolean;
|
||||
private: boolean;
|
||||
refundAddress?: string;
|
||||
refundTxId?: string;
|
||||
}
|
||||
|
|
|
@ -792,9 +792,10 @@
|
|||
reflect-metadata "^0.1.12"
|
||||
tslib "^1.8.1"
|
||||
|
||||
"@nx-js/observer-util@^4.1.1":
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@nx-js/observer-util/-/observer-util-4.2.0.tgz#ec9e2f903dda94cc3d8ac077617bc369f2ad2d6e"
|
||||
"@nx-js/observer-util@^4.2.2":
|
||||
version "4.2.2"
|
||||
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":
|
||||
version "0.3.0"
|
||||
|
@ -6616,11 +6617,12 @@ react-dom@16.5.2:
|
|||
prop-types "^15.6.2"
|
||||
schedule "^0.5.0"
|
||||
|
||||
react-easy-state@^6.0.4:
|
||||
version "6.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-easy-state/-/react-easy-state-6.0.4.tgz#94a124fe69723abcb922c15059dd444b2e3499f3"
|
||||
react-easy-state@^6.1.3:
|
||||
version "6.1.3"
|
||||
resolved "https://registry.yarnpkg.com/react-easy-state/-/react-easy-state-6.1.3.tgz#f9db4e8d842b5acfb73b6899aaf49a26900f7d26"
|
||||
integrity sha512-uWQ7ittvJylwn/Xgz7Ub1jjsbpthQ9Ad1KDLxXfbXCb2OPnov4porVdnOJU2PKeRezcam3ZgfPUtf9L9rjtyWg==
|
||||
dependencies:
|
||||
"@nx-js/observer-util" "^4.1.1"
|
||||
"@nx-js/observer-util" "^4.2.2"
|
||||
|
||||
react-error-overlay@^4.0.1:
|
||||
version "4.0.1"
|
||||
|
|
|
@ -156,7 +156,6 @@ def stats():
|
|||
contribution_refundable_count = db.session.query(func.count(ProposalContribution.id)) \
|
||||
.filter(ProposalContribution.refund_tx_id == None) \
|
||||
.filter(ProposalContribution.staking == False) \
|
||||
.filter(ProposalContribution.no_refund == False) \
|
||||
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
|
||||
.join(Proposal) \
|
||||
.filter(or_(
|
||||
|
@ -609,7 +608,7 @@ def get_contribution(contribution_id):
|
|||
@body({
|
||||
"proposalId": 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),
|
||||
"txId": fields.Str(required=False, missing=None),
|
||||
"refundTxId": fields.Str(required=False, allow_none=True, missing=None),
|
||||
|
@ -723,6 +722,8 @@ def financials():
|
|||
SELECT SUM(TO_NUMBER(amount, '{nfmt}'))
|
||||
FROM proposal_contribution as pc
|
||||
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}
|
||||
'''
|
||||
|
||||
|
@ -743,14 +744,34 @@ def financials():
|
|||
'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'"))),
|
||||
'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(
|
||||
"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(
|
||||
"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(
|
||||
"(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"))),
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ class Comment(db.Model):
|
|||
self.proposal_id = proposal_id
|
||||
self.user_id = user_id
|
||||
self.parent_comment_id = parent_comment_id
|
||||
self.content = content[:1000]
|
||||
self.content = content[:5000]
|
||||
self.date_created = datetime.datetime.now()
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -88,7 +88,7 @@ class ProposalContribution(db.Model):
|
|||
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)
|
||||
no_refund = db.Column(db.Boolean, nullable=False)
|
||||
private = db.Column(db.Boolean, nullable=False, default=False, server_default='true')
|
||||
|
||||
user = db.relationship("User")
|
||||
|
||||
|
@ -98,23 +98,23 @@ class ProposalContribution(db.Model):
|
|||
amount: str,
|
||||
user_id: int = None,
|
||||
staking: bool = False,
|
||||
no_refund: bool = False,
|
||||
private: bool = True,
|
||||
):
|
||||
self.proposal_id = proposal_id
|
||||
self.amount = amount
|
||||
self.user_id = user_id
|
||||
self.staking = staking
|
||||
self.no_refund = no_refund
|
||||
self.private = private
|
||||
self.date_created = datetime.datetime.now()
|
||||
self.status = ContributionStatus.PENDING
|
||||
|
||||
@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(
|
||||
user_id=user_id,
|
||||
proposal_id=proposal_id,
|
||||
amount=amount,
|
||||
no_refund=no_refund,
|
||||
private=private,
|
||||
status=ContributionStatus.PENDING,
|
||||
).first()
|
||||
|
||||
|
@ -425,14 +425,14 @@ class Proposal(db.Model):
|
|||
amount,
|
||||
user_id: int = None,
|
||||
staking: bool = False,
|
||||
no_refund: bool = False,
|
||||
private: bool = True,
|
||||
):
|
||||
contribution = ProposalContribution(
|
||||
proposal_id=self.id,
|
||||
amount=amount,
|
||||
user_id=user_id,
|
||||
staking=staking,
|
||||
no_refund=no_refund,
|
||||
private=private
|
||||
)
|
||||
db.session.add(contribution)
|
||||
db.session.flush()
|
||||
|
@ -841,6 +841,7 @@ class ProposalContributionSchema(ma.Schema):
|
|||
"date_created",
|
||||
"addresses",
|
||||
"is_anonymous",
|
||||
"private"
|
||||
)
|
||||
|
||||
proposal = ma.Nested("ProposalSchema")
|
||||
|
@ -861,11 +862,11 @@ class ProposalContributionSchema(ma.Schema):
|
|||
}
|
||||
|
||||
def get_is_anonymous(self, obj):
|
||||
return not obj.user_id
|
||||
return not obj.user_id or obj.private
|
||||
|
||||
@post_dump
|
||||
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
|
||||
return data
|
||||
|
||||
|
@ -894,7 +895,7 @@ class AdminProposalContributionSchema(ma.Schema):
|
|||
"refund_address",
|
||||
"refund_tx_id",
|
||||
"staking",
|
||||
"no_refund",
|
||||
"private",
|
||||
)
|
||||
|
||||
proposal = ma.Nested("ProposalSchema")
|
||||
|
|
|
@ -99,7 +99,7 @@ def report_proposal_comment(proposal_id, comment_id):
|
|||
@limiter.limit("30/hour;2/minute")
|
||||
@requires_email_verified_auth
|
||||
@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),
|
||||
})
|
||||
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")
|
||||
@body({
|
||||
"amount": fields.Str(required=True, validate=lambda p: 0.0001 <= float(p) <= 1000000),
|
||||
"anonymous": fields.Bool(required=False, missing=None),
|
||||
"noRefund": fields.Bool(required=False, missing=False),
|
||||
"private": fields.Bool(required=False, missing=True)
|
||||
})
|
||||
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()
|
||||
if not proposal:
|
||||
return {"message": "No proposal matching id"}, 404
|
||||
|
||||
code = 200
|
||||
user = None
|
||||
user = get_authed_user()
|
||||
contribution = None
|
||||
|
||||
if not anonymous:
|
||||
user = get_authed_user()
|
||||
|
||||
if user:
|
||||
contribution = ProposalContribution \
|
||||
.get_existing_contribution(user.id, proposal_id, amount, no_refund)
|
||||
.get_existing_contribution(user.id, proposal_id, amount, private)
|
||||
|
||||
if not contribution:
|
||||
code = 201
|
||||
contribution = proposal.create_contribution(
|
||||
amount=amount,
|
||||
no_refund=no_refund,
|
||||
private=private,
|
||||
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)
|
||||
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 not c.private]
|
||||
contributions = [c for c in contributions if c.proposal.status == ProposalStatus.LIVE]
|
||||
contributions_dump = user_proposal_contributions_schema.dump(contributions)
|
||||
result["contributions"] = contributions_dump
|
||||
|
|
|
@ -159,7 +159,6 @@ class ContributionPagination(Pagination):
|
|||
if 'REFUNDABLE' in filters:
|
||||
query = query.filter(ProposalContribution.refund_tx_id == None) \
|
||||
.filter(ProposalContribution.staking == False) \
|
||||
.filter(ProposalContribution.no_refund == False) \
|
||||
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
|
||||
.join(Proposal) \
|
||||
.filter(or_(
|
||||
|
@ -173,15 +172,14 @@ class ContributionPagination(Pagination):
|
|||
if 'DONATION' in filters:
|
||||
query = query.filter(ProposalContribution.refund_tx_id == None) \
|
||||
.filter(ProposalContribution.status == ContributionStatus.CONFIRMED) \
|
||||
.filter(or_(
|
||||
ProposalContribution.no_refund == True,
|
||||
ProposalContribution.user_id == None,
|
||||
)) \
|
||||
.join(Proposal) \
|
||||
.filter(or_(
|
||||
Proposal.stage == ProposalStage.FAILED,
|
||||
Proposal.stage == ProposalStage.CANCELED,
|
||||
))
|
||||
)) \
|
||||
.join(ProposalContribution.user, isouter=True) \
|
||||
.join(UserSettings, isouter=True) \
|
||||
.filter(UserSettings.refund_address == None)
|
||||
|
||||
# SORT (see self.SORT_MAP)
|
||||
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 .mocks import mock_request
|
||||
|
||||
test_user = {
|
||||
|
@ -54,22 +55,71 @@ test_comment = {
|
|||
|
||||
test_comment_large = {
|
||||
"comment": """
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
"""
|
||||
massa vitae tortor condimentum lacinia quis vel eros donec ac odio tempor orci dapibus ultrices
|
||||
in iaculis nunc sed augue lacus viverra vitae congue eu consequat ac felis donec et odio
|
||||
pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus
|
||||
faucibus scelerisque eleifend donec pretium vulputate sapien nec
|
||||
sagittis aliquam malesuada bibendum arcu vitae elementum curabitur
|
||||
vitae nunc sed velit dignissim sodales ut eu sem integer vitae justo
|
||||
eget magna fermentum iaculis eu non diam phasellus vestibulum lorem sed
|
||||
risus ultricies tristique nulla aliquet enim tortor at auctor urna nunc id
|
||||
cursus metus aliquam eleifend mi in nulla posuere sollicitudin aliquam ultrices
|
||||
sagittis orci a scelerisque purus semper eget duis at tellus at urna condimentum
|
||||
mattis pellentesque id nibh tortor id aliquet lectus proin nibh nisl condimentum id
|
||||
venenatis a condimentum vitae sapien pellentesque habitant morbi tristique senectus
|
||||
et netus et malesuada fames ac turpis egestas sed tempus urna et pharetra pharetra
|
||||
massa massa ultricies mi quis hendrerit dolor magna eget est lorem ipsum dolor sit
|
||||
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
|
||||
"""
|
||||
}
|
||||
|
||||
test_reply = {
|
||||
|
|
|
@ -316,13 +316,11 @@ export function putInviteResponse(
|
|||
export function postProposalContribution(
|
||||
proposalId: number,
|
||||
amount: string,
|
||||
anonymous: boolean = false,
|
||||
noRefund: boolean = false,
|
||||
isPrivate: boolean = true,
|
||||
): Promise<{ data: ContributionWithAddressesAndUser }> {
|
||||
return axios.post(`/api/v1/proposals/${proposalId}/contributions`, {
|
||||
amount,
|
||||
anonymous,
|
||||
noRefund,
|
||||
private: isPrivate,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ interface OwnProps {
|
|||
contributionId?: number;
|
||||
amount?: string;
|
||||
isAnonymous?: boolean;
|
||||
isPublic?: boolean;
|
||||
hasNoButtons?: boolean;
|
||||
text?: React.ReactNode;
|
||||
handleClose(): void;
|
||||
|
@ -127,17 +128,13 @@ class ContributionModal extends React.Component<Props, State> {
|
|||
message="This contribution will not be attributed"
|
||||
description={
|
||||
<>
|
||||
Your contribution will show up without attribution. Even if you're logged
|
||||
in, the contribution will not appear anywhere on your account after you
|
||||
close this modal.
|
||||
ZF Grants is unable to offer refunds for contributions made without
|
||||
accounts with a set refund address. If refunds for this campaign are issued, your contribution will be
|
||||
treated as a donation to the Zcash Foundation.
|
||||
<br /> <br />
|
||||
ZF Grants is unable to offer refunds for non-attributed contributions. If
|
||||
refunds for this campaign are issued, your contribution will be treated as a
|
||||
donation to the Zcash Foundation.
|
||||
<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.
|
||||
If you would like your contribution to be eligible for refund, you can close
|
||||
this modal, make sure you're logged in, set a refund address, and attempt to contribute again. You
|
||||
can still choose to contribute without public attribution while logged in.
|
||||
<br /> <br />
|
||||
NOTE: The Zcash Foundation is unable to accept donations of more than $5,000
|
||||
USD worth of ZEC from anonymous users.
|
||||
|
@ -221,7 +218,7 @@ class ContributionModal extends React.Component<Props, State> {
|
|||
) {
|
||||
this.setState({ isFetchingContribution: true });
|
||||
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
|
||||
if (!isAnonymous && !noRefund) {
|
||||
|
@ -240,12 +237,7 @@ class ContributionModal extends React.Component<Props, State> {
|
|||
if (contributionId) {
|
||||
res = await getProposalContribution(proposalId, contributionId);
|
||||
} else {
|
||||
res = await postProposalContribution(
|
||||
proposalId,
|
||||
amount || '0',
|
||||
isAnonymous,
|
||||
noRefund,
|
||||
);
|
||||
res = await postProposalContribution(proposalId, amount || '0', !isPublic);
|
||||
}
|
||||
this.setState({ contribution: res.data });
|
||||
} catch (err) {
|
||||
|
|
|
@ -39,10 +39,7 @@ const commands: { [key in MARKDOWN_TYPE]: ReactMdeProps['commands'] } = {
|
|||
],
|
||||
},
|
||||
{
|
||||
commands: [
|
||||
ReactMdeCommands.linkCommand,
|
||||
ReactMdeCommands.quoteCommand,
|
||||
],
|
||||
commands: [ReactMdeCommands.linkCommand, ReactMdeCommands.quoteCommand],
|
||||
},
|
||||
{
|
||||
commands: [
|
||||
|
@ -104,7 +101,7 @@ export default class MarkdownEditor extends React.PureComponent<Props, State> {
|
|||
const { randomKey, value, tab } = this.state;
|
||||
return (
|
||||
<div
|
||||
ref={(el) => this.el = el}
|
||||
ref={el => (this.el = el)}
|
||||
className={classnames({
|
||||
MarkdownEditor: true,
|
||||
['is-reduced']: type === MARKDOWN_TYPE.REDUCED,
|
||||
|
@ -119,6 +116,7 @@ export default class MarkdownEditor extends React.PureComponent<Props, State> {
|
|||
generateMarkdownPreview={this.generatePreview}
|
||||
commands={commands[type]}
|
||||
readOnly={!!readOnly}
|
||||
textAreaProps={{ maxLength: 5000 }}
|
||||
minEditorHeight={minHeight}
|
||||
minPreviewHeight={minHeight - 10}
|
||||
maxEditorHeight={99999}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Tag, Popconfirm } from 'antd';
|
||||
import { Tag, Popconfirm, Tooltip } from 'antd';
|
||||
import UnitDisplay from 'components/UnitDisplay';
|
||||
import { ONE_DAY } from 'utils/time';
|
||||
import { formatTxExplorerUrl } from 'utils/formatters';
|
||||
|
@ -25,7 +25,7 @@ type Props = OwnProps & DispatchProps;
|
|||
class ProfileContribution extends React.Component<Props> {
|
||||
render() {
|
||||
const { contribution } = this.props;
|
||||
const { proposal } = contribution;
|
||||
const { proposal, private: isPrivate } = contribution;
|
||||
const isConfirmed = contribution.status === 'CONFIRMED';
|
||||
const isExpired =
|
||||
(!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 (
|
||||
<div className="ProfileContribution">
|
||||
<div className="ProfileContribution-info">
|
||||
|
@ -70,7 +82,8 @@ class ProfileContribution extends React.Component<Props> {
|
|||
className="ProfileContribution-info-title"
|
||||
to={`/proposals/${proposal.proposalId}`}
|
||||
>
|
||||
{proposal.title} {tag}
|
||||
{proposal.title} {privateTag}
|
||||
{tag}
|
||||
</Link>
|
||||
<div className="ProfileContribution-info-brief">{proposal.brief}</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { Form, Input, Checkbox, Button, Icon, Popover, Tooltip } from 'antd';
|
||||
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
|
||||
import { Form, Input, Button, Icon, Popover, Tooltip, Radio } from 'antd';
|
||||
import { RadioChangeEvent } from 'antd/lib/radio';
|
||||
import { Proposal, STATUS } from 'types';
|
||||
import classnames from 'classnames';
|
||||
import { fromZat } from 'utils/units';
|
||||
|
@ -30,7 +30,7 @@ type Props = OwnProps & StateProps;
|
|||
interface State {
|
||||
amountToRaise: string;
|
||||
amountError: string | null;
|
||||
isAnonymous: boolean;
|
||||
isPrivate: boolean;
|
||||
isContributing: boolean;
|
||||
}
|
||||
|
||||
|
@ -40,14 +40,14 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
|||
this.state = {
|
||||
amountToRaise: '',
|
||||
amountError: null,
|
||||
isAnonymous: false,
|
||||
isPrivate: true,
|
||||
isContributing: false,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
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;
|
||||
let content;
|
||||
if (proposal) {
|
||||
|
@ -190,12 +190,23 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
|||
</Form.Item>
|
||||
{amountToRaise &&
|
||||
!!authUser && (
|
||||
<Checkbox checked={isAnonymous} onChange={this.handleChangeAnonymity}>
|
||||
Contribute without attribution
|
||||
<Tooltip title="Your contribution will not be linked to your account. ZF Grants cannot refund non-attributed contributions.">
|
||||
<Icon type="question-circle" />
|
||||
</Tooltip>
|
||||
</Checkbox>
|
||||
<Radio.Group
|
||||
onChange={this.handleChangePrivate}
|
||||
value={isPrivate ? 'isPrivate' : 'isNotPrivate'}
|
||||
>
|
||||
<Radio value={'isPrivate'}>
|
||||
Contribute without attribution
|
||||
<Tooltip title="Other users will not see who made this contribution.">
|
||||
<Icon type="question-circle" />
|
||||
</Tooltip>
|
||||
</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
|
||||
onClick={this.openContributionModal}
|
||||
|
@ -214,7 +225,8 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
|||
isVisible={isContributing}
|
||||
proposalId={proposal.proposalId}
|
||||
amount={amountToRaise}
|
||||
isAnonymous={isAnonymous || !authUser}
|
||||
isAnonymous={!authUser}
|
||||
isPublic={!isPrivate}
|
||||
handleClose={this.closeContributionModal}
|
||||
/>
|
||||
</React.Fragment>
|
||||
|
@ -255,8 +267,9 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
|||
this.setState({ amountToRaise: value, amountError });
|
||||
};
|
||||
|
||||
private handleChangeAnonymity = (ev: CheckboxChangeEvent) => {
|
||||
this.setState({ isAnonymous: ev.target.checked });
|
||||
private handleChangePrivate = (ev: RadioChangeEvent) => {
|
||||
const isPrivate = ev.target.value === 'isPrivate';
|
||||
this.setState({ isPrivate });
|
||||
};
|
||||
|
||||
private openContributionModal = () => this.setState({ isContributing: true });
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
margin: 0.5rem -1.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: center;
|
||||
color: #FFF;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
|
||||
.anticon {
|
||||
|
@ -57,7 +57,6 @@
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
||||
&-popover {
|
||||
&-overlay {
|
||||
max-width: 400px;
|
||||
|
@ -91,7 +90,7 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
.ant-radio-wrapper {
|
||||
margin-bottom: 0.5rem;
|
||||
opacity: 0.7;
|
||||
font-size: 0.8rem;
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
LoadableProposalPage,
|
||||
Moreable,
|
||||
} from 'types';
|
||||
import { PROPOSAL_SORT, PROPOSAL_STAGE } from 'api/constants';
|
||||
import { PROPOSAL_SORT } from 'api/constants';
|
||||
|
||||
export interface ProposalDetail extends Proposal {
|
||||
isRequestingPayout: boolean;
|
||||
|
@ -62,7 +62,7 @@ export const INITIAL_STATE: ProposalState = {
|
|||
sort: PROPOSAL_SORT.NEWEST,
|
||||
filters: {
|
||||
category: [],
|
||||
stage: [PROPOSAL_STAGE.FUNDING_REQUIRED],
|
||||
stage: [],
|
||||
},
|
||||
items: [],
|
||||
hasFetched: false,
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
* 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 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;
|
||||
txId?: string;
|
||||
proposal: Proposal;
|
||||
private: boolean;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue