Merge pull request #459 from ZcashFoundation/develop

Release 1.4.0
This commit is contained in:
Daniel Ternyak 2019-06-11 21:51:33 -05:00 committed by GitHub
commit dd966ae853
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 257 additions and 132 deletions

View File

@ -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",

View File

@ -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>

View File

@ -152,7 +152,7 @@ export interface Contribution {
memo: string;
};
staking: boolean;
noRefund: boolean;
private: boolean;
refundAddress?: string;
refundTxId?: string;
}

View File

@ -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"

View File

@ -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"))),
}

View File

@ -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

View File

@ -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")

View File

@ -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,
)

View File

@ -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

View File

@ -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:

View File

@ -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 ###

View File

@ -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 = {

View File

@ -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,
});
}

View File

@ -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) {

View File

@ -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}

View File

@ -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>

View File

@ -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 });

View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -30,4 +30,5 @@ export interface UserContribution extends Omit<Contribution, 'amount' | 'txId'>
amount: Zat;
txId?: string;
proposal: Proposal;
private: boolean;
}