diff --git a/admin/package.json b/admin/package.json index 8bded901..ada72135 100644 --- a/admin/package.json +++ b/admin/package.json @@ -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", diff --git a/admin/src/components/ContributionDetail/index.tsx b/admin/src/components/ContributionDetail/index.tsx index 767d24cc..63662c4e 100644 --- a/admin/src/components/ContributionDetail/index.tsx +++ b/admin/src/components/ContributionDetail/index.tsx @@ -36,23 +36,27 @@ class ContributionDetail extends React.Component { const renderDeetItem = (label: string, val: React.ReactNode) => (
-
- {val} -
-
- {label} -
+
{val}
+
{label}
); 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 ( { description={

- The proposal this contribution was made towards has failed, and - is ready to be refunded to {c.user.displayName} - {' '}for {percent}% of the contribution amount. - Please Make a payment of {fromZat(amount)} ZEC to: + The proposal this contribution was made towards has failed, and is ready + to be refunded to {c.user.displayName} for{' '} + {percent}% of the contribution amount. Please Make a + payment of {fromZat(amount)} ZEC to:

{c.refundAddress}

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

{
{JSON.stringify(c.addresses, null, 4)}
- - - {c.user ? : Anonymous contribution} - + + {c.user ? : Anonymous contribution} + @@ -118,7 +121,9 @@ class ContributionDetail extends React.Component { {/* ACTIONS */} - + @@ -128,16 +133,20 @@ class ContributionDetail extends React.Component { {renderDeetItem('created', formatDateSeconds(c.dateCreated))} {renderDeetItem('status', c.status)} {renderDeetItem('amount', c.amount)} - {renderDeetItem('txid', c.txId - ? - : N/A + {renderDeetItem( + 'txid', + c.txId ? : N/A, )} - {renderDeetItem('refund txid', c.refundTxId - ? - : N/A + {renderDeetItem( + 'refund txid', + c.refundTxId ? ( + + ) : ( + N/A + ), )} {renderDeetItem('staking tx', JSON.stringify(c.staking))} - {renderDeetItem('no refund', JSON.stringify(c.noRefund))} + {renderDeetItem('private', JSON.stringify(c.private))} diff --git a/admin/src/types.ts b/admin/src/types.ts index 72f75e63..7129194d 100644 --- a/admin/src/types.ts +++ b/admin/src/types.ts @@ -152,7 +152,7 @@ export interface Contribution { memo: string; }; staking: boolean; - noRefund: boolean; + private: boolean; refundAddress?: string; refundTxId?: string; } diff --git a/admin/yarn.lock b/admin/yarn.lock index 8a9334fc..d63adeb2 100644 --- a/admin/yarn.lock +++ b/admin/yarn.lock @@ -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" diff --git a/backend/grant/admin/views.py b/backend/grant/admin/views.py index 44ba82b8..e9916a1b 100644 --- a/backend/grant/admin/views.py +++ b/backend/grant/admin/views.py @@ -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"))), } diff --git a/backend/grant/comment/models.py b/backend/grant/comment/models.py index 6b57c3c5..395be41b 100644 --- a/backend/grant/comment/models.py +++ b/backend/grant/comment/models.py @@ -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 diff --git a/backend/grant/proposal/models.py b/backend/grant/proposal/models.py index 73dd1ae3..ea9d4c91 100644 --- a/backend/grant/proposal/models.py +++ b/backend/grant/proposal/models.py @@ -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") diff --git a/backend/grant/proposal/views.py b/backend/grant/proposal/views.py index ebda9083..eba7ed00 100644 --- a/backend/grant/proposal/views.py +++ b/backend/grant/proposal/views.py @@ -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, ) diff --git a/backend/grant/user/views.py b/backend/grant/user/views.py index cc024781..622e8959 100644 --- a/backend/grant/user/views.py +++ b/backend/grant/user/views.py @@ -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 diff --git a/backend/grant/utils/pagination.py b/backend/grant/utils/pagination.py index 65022eda..3d02e0eb 100644 --- a/backend/grant/utils/pagination.py +++ b/backend/grant/utils/pagination.py @@ -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: diff --git a/backend/migrations/versions/4505f00c4ebd_.py b/backend/migrations/versions/4505f00c4ebd_.py new file mode 100644 index 00000000..d0ba4981 --- /dev/null +++ b/backend/migrations/versions/4505f00c4ebd_.py @@ -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 ### diff --git a/backend/tests/test_data.py b/backend/tests/test_data.py index 969db449..d7c323c3 100644 --- a/backend/tests/test_data.py +++ b/backend/tests/test_data.py @@ -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 = { diff --git a/frontend/client/api/api.ts b/frontend/client/api/api.ts index 0307f804..5f150056 100644 --- a/frontend/client/api/api.ts +++ b/frontend/client/api/api.ts @@ -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, }); } diff --git a/frontend/client/components/ContributionModal/index.tsx b/frontend/client/components/ContributionModal/index.tsx index dea0d397..ea6e9726 100644 --- a/frontend/client/components/ContributionModal/index.tsx +++ b/frontend/client/components/ContributionModal/index.tsx @@ -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 { 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.

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

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

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 { ) { 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 { 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) { diff --git a/frontend/client/components/MarkdownEditor/index.tsx b/frontend/client/components/MarkdownEditor/index.tsx index a0e86e64..d534bf5f 100644 --- a/frontend/client/components/MarkdownEditor/index.tsx +++ b/frontend/client/components/MarkdownEditor/index.tsx @@ -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 { const { randomKey, value, tab } = this.state; return (
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 { generateMarkdownPreview={this.generatePreview} commands={commands[type]} readOnly={!!readOnly} + textAreaProps={{ maxLength: 5000 }} minEditorHeight={minHeight} minPreviewHeight={minHeight - 10} maxEditorHeight={99999} diff --git a/frontend/client/components/Profile/ProfileContribution.tsx b/frontend/client/components/Profile/ProfileContribution.tsx index 545e777e..52a5147b 100644 --- a/frontend/client/components/Profile/ProfileContribution.tsx +++ b/frontend/client/components/Profile/ProfileContribution.tsx @@ -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 { 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 { ); } + const privateTag = isPrivate ? ( + + Other users will not be able to see that you made this contribution. + + } + > + Private + + ) : null; + return (
@@ -70,7 +82,8 @@ class ProfileContribution extends React.Component { className="ProfileContribution-info-title" to={`/proposals/${proposal.proposalId}`} > - {proposal.title} {tag} + {proposal.title} {privateTag} + {tag}
{proposal.brief}
diff --git a/frontend/client/components/Proposal/CampaignBlock/index.tsx b/frontend/client/components/Proposal/CampaignBlock/index.tsx index 61f92670..12123d22 100644 --- a/frontend/client/components/Proposal/CampaignBlock/index.tsx +++ b/frontend/client/components/Proposal/CampaignBlock/index.tsx @@ -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 { 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 { {amountToRaise && !!authUser && ( - - Contribute without attribution - - - - + + + Contribute without attribution + + + + + + Attribute contribution publicly + + + + + )}