From db05d2b86fa2d75f162f5c3f83a21ab8552b452a Mon Sep 17 00:00:00 2001
From: Masala
Date: Fri, 15 Mar 2019 17:58:19 -0400
Subject: [PATCH 01/26] no negative payouts
---
backend/grant/proposal/models.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/grant/proposal/models.py b/backend/grant/proposal/models.py
index 1224ed5a..7c80998c 100644
--- a/backend/grant/proposal/models.py
+++ b/backend/grant/proposal/models.py
@@ -299,7 +299,7 @@ class Proposal(db.Model):
if len(milestone.content) > 200:
raise ValidationException("Milestone content must be no more than 200 chars")
- payout_total += float(milestone.payout_percent)
+ payout_total += abs(float(milestone.payout_percent))
try:
present = datetime.datetime.today().replace(day=1)
From 7703d274bdfe94c42ab5b9d3c98214d9669947eb Mon Sep 17 00:00:00 2001
From: Masala
Date: Fri, 15 Mar 2019 19:17:55 -0400
Subject: [PATCH 02/26] raise ValidationException on invalid payout percent
---
backend/grant/proposal/models.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/backend/grant/proposal/models.py b/backend/grant/proposal/models.py
index 7c80998c..91dd7250 100644
--- a/backend/grant/proposal/models.py
+++ b/backend/grant/proposal/models.py
@@ -299,7 +299,15 @@ class Proposal(db.Model):
if len(milestone.content) > 200:
raise ValidationException("Milestone content must be no more than 200 chars")
- payout_total += abs(float(milestone.payout_percent))
+ try:
+ p = float(milestone.payout_percent)
+ if p <= 0:
+ raise ValidationException("Milestone payout percent must be greater than zero")
+
+ except ValueError:
+ raise ValidationException("Milestone payout percent must be a number")
+
+ payout_total += p
try:
present = datetime.datetime.today().replace(day=1)
From 3baa6d258f8c9427e770c14c6252f07aa67326e4 Mon Sep 17 00:00:00 2001
From: Masala
Date: Fri, 15 Mar 2019 19:26:15 -0400
Subject: [PATCH 03/26] add payout percent frontend validation
---
frontend/client/modules/create/utils.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/frontend/client/modules/create/utils.ts b/frontend/client/modules/create/utils.ts
index 123f6343..27ed8d5e 100644
--- a/frontend/client/modules/create/utils.ts
+++ b/frontend/client/modules/create/utils.ts
@@ -134,6 +134,8 @@ export function getCreateErrors(
return 'Payout percent is required';
} else if (Number.isNaN(parseInt(ms.payoutPercent, 10))) {
return 'Payout percent must be a valid number';
+ } else if (parseInt(ms.payoutPercent, 10) <= 0) {
+ return 'Payout percent must be greater than zero';
}
// Last one shows percentage errors
From ba1b54d979475058393c96d0477cd4d6ee537ed2 Mon Sep 17 00:00:00 2001
From: Masala
Date: Fri, 15 Mar 2019 19:31:59 -0400
Subject: [PATCH 04/26] add check for payout percent > 100
---
frontend/client/modules/create/utils.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/frontend/client/modules/create/utils.ts b/frontend/client/modules/create/utils.ts
index 27ed8d5e..dd9399f9 100644
--- a/frontend/client/modules/create/utils.ts
+++ b/frontend/client/modules/create/utils.ts
@@ -136,6 +136,8 @@ export function getCreateErrors(
return 'Payout percent must be a valid number';
} else if (parseInt(ms.payoutPercent, 10) <= 0) {
return 'Payout percent must be greater than zero';
+ } else if (parseInt(ms.payoutPercent, 10) > 100) {
+ return 'Payout percent must be less than or equal to 100';
}
// Last one shows percentage errors
From 6fc6fedab7cdbd4c7ba638cc56bae0db3bdfc50e Mon Sep 17 00:00:00 2001
From: Will O'Beirne
Date: Mon, 18 Mar 2019 12:26:57 -0400
Subject: [PATCH 05/26] Prevent brief from overflowing.
---
.../components/Profile/ProfileInvite.less | 3 +++
frontend/client/styles/antd-overrides.less | 17 +++++++++++++++++
2 files changed, 20 insertions(+)
diff --git a/frontend/client/components/Profile/ProfileInvite.less b/frontend/client/components/Profile/ProfileInvite.less
index 22c82983..e125a882 100644
--- a/frontend/client/components/Profile/ProfileInvite.less
+++ b/frontend/client/components/Profile/ProfileInvite.less
@@ -7,6 +7,9 @@
margin-bottom: 1rem;
&-info {
+ min-width: 0;
+ padding-right: 2rem;
+
&-title {
font-size: 1.2rem;
font-weight: 600;
diff --git a/frontend/client/styles/antd-overrides.less b/frontend/client/styles/antd-overrides.less
index b57ba7a1..e67a80a3 100644
--- a/frontend/client/styles/antd-overrides.less
+++ b/frontend/client/styles/antd-overrides.less
@@ -16,3 +16,20 @@
div.antd-pro-ellipsis-ellipsis {
word-break: break-word;
}
+
+// List items with long content can push the actions aside
+.ant-list-item {
+ overflow: hidden;
+
+ .ant-list-item-content,
+ .ant-list-item-meta,
+ .ant-list-item-meta-content {
+ min-width: 0;
+ }
+
+ .ant-list-item-meta-title,
+ .ant-list-item-meta-description {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+}
\ No newline at end of file
From 2eff058add540a3d98842a397b63d57c03fcdc77 Mon Sep 17 00:00:00 2001
From: Will O'Beirne
Date: Mon, 18 Mar 2019 12:43:30 -0400
Subject: [PATCH 06/26] Rehydrate proposal detail page milestones.
---
frontend/client/utils/api.ts | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/frontend/client/utils/api.ts b/frontend/client/utils/api.ts
index 0d0f4a89..984d1ffa 100644
--- a/frontend/client/utils/api.ts
+++ b/frontend/client/utils/api.ts
@@ -149,6 +149,10 @@ export function massageSerializedState(state: AppState) {
);
state.proposal.detail.contributionBounty = new BN((state.proposal.detail
.contributionBounty as any) as string);
+ state.proposal.detail.milestones = state.proposal.detail.milestones.map(m => ({
+ ...m,
+ amount: new BN((m.amount as any) as string, 16),
+ }));
if (state.proposal.detail.rfp && state.proposal.detail.rfp.bounty) {
state.proposal.detail.rfp.bounty = new BN(
(state.proposal.detail.rfp.bounty as any) as string,
From b0c6614a6454009990f073db88c803eaea4b36bb Mon Sep 17 00:00:00 2001
From: Will O'Beirne
Date: Mon, 18 Mar 2019 12:51:46 -0400
Subject: [PATCH 07/26] Dont allow inviting members already on the team
(including yourself.)
---
backend/grant/proposal/views.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/backend/grant/proposal/views.py b/backend/grant/proposal/views.py
index 3893a98c..8064b501 100644
--- a/backend/grant/proposal/views.py
+++ b/backend/grant/proposal/views.py
@@ -379,6 +379,10 @@ def post_proposal_update(proposal_id, title, content):
"address": fields.Str(required=True),
})
def post_proposal_team_invite(proposal_id, address):
+ for u in g.current_proposal.team:
+ if address == u.email_address:
+ return {"message": f"Cannot invite members already on the team"}, 400
+
existing_invite = ProposalTeamInvite.query.filter_by(
proposal_id=proposal_id,
address=address
From adc2fd4d63c4aa9d36be6b3ef7113e23406eccb3 Mon Sep 17 00:00:00 2001
From: Will O'Beirne
Date: Mon, 18 Mar 2019 14:35:08 -0400
Subject: [PATCH 08/26] Stricter validation, truncate before db entry, env var
proposal target limit
---
backend/.env.example | 5 +-
backend/grant/milestone/models.py | 11 ++---
backend/grant/proposal/models.py | 48 +++++++++++++------
backend/grant/proposal/views.py | 3 +-
backend/grant/settings.py | 1 +
frontend/.env.example | 5 +-
.../client/components/CreateFlow/Basics.tsx | 3 ++
.../client/components/CreateFlow/Details.tsx | 6 ++-
.../client/components/CreateFlow/index.less | 4 ++
.../client/components/CreateFlow/index.tsx | 26 ++++++----
frontend/client/modules/create/utils.ts | 22 +++++++--
frontend/config/env.js | 1 +
12 files changed, 95 insertions(+), 40 deletions(-)
diff --git a/backend/.env.example b/backend/.env.example
index 736aa204..d4c83bff 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -32,5 +32,8 @@ BLOCKCHAIN_API_SECRET="ef0b48e41f78d3ae85b1379b386f1bca"
# EXPLORER_URL="https://chain.so/tx/ZEC/"
EXPLORER_URL="https://chain.so/tx/ZECTEST/"
-# Amount for staking a proposal in ZEC
+# Amount for staking a proposal in ZEC, keep in sync with frontend .env
PROPOSAL_STAKING_AMOUNT=0.025
+
+# Maximum amount for a proposal target, keep in sync with frontend .env
+PROPOSAL_TARGET_MAX=10000
diff --git a/backend/grant/milestone/models.py b/backend/grant/milestone/models.py
index cab8d261..36f5976d 100644
--- a/backend/grant/milestone/models.py
+++ b/backend/grant/milestone/models.py
@@ -70,21 +70,16 @@ class Milestone(db.Model):
[db.session.delete(x) for x in proposal.milestones]
for i, milestone_data in enumerate(milestones_data):
m = Milestone(
- title=milestone_data["title"],
- content=milestone_data["content"],
+ title=milestone_data["title"][:255],
+ content=milestone_data["content"][:255],
date_estimated=datetime.datetime.fromtimestamp(milestone_data["date_estimated"]),
- payout_percent=str(milestone_data["payout_percent"]),
+ payout_percent=str(milestone_data["payout_percent"])[:255],
immediate_payout=milestone_data["immediate_payout"],
proposal_id=proposal.id,
index=i
)
db.session.add(m)
- @staticmethod
- def validate(milestone):
- if len(milestone.title) > 60:
- raise ValidationException("Milestone title must be no more than 60 chars")
-
def request_payout(self, user_id: int):
if self.stage not in [MilestoneStage.IDLE, MilestoneStage.REJECTED]:
raise MilestoneException(f'Cannot request payout for milestone at {self.stage} stage')
diff --git a/backend/grant/proposal/models.py b/backend/grant/proposal/models.py
index 1224ed5a..90a481ad 100644
--- a/backend/grant/proposal/models.py
+++ b/backend/grant/proposal/models.py
@@ -11,7 +11,7 @@ from flask import current_app
from grant.comment.models import Comment
from grant.email.send import send_email
from grant.extensions import ma, db
-from grant.settings import PROPOSAL_STAKING_AMOUNT
+from grant.settings import PROPOSAL_STAKING_AMOUNT, PROPOSAL_TARGET_MAX
from grant.task.jobs import ContributionExpired
from grant.utils.enums import (
ProposalStatus,
@@ -275,12 +275,11 @@ class Proposal(db.Model):
@staticmethod
def simple_validate(proposal):
- title = proposal.get('title')
+ # Validate fields to be database save-able.
+ # Stricter validation is done in validate_publishable.
stage = proposal.get('stage')
category = proposal.get('category')
- if title and len(title) > 60:
- raise ValidationException("Proposal title cannot be longer than 60 characters")
if stage and not ProposalStage.includes(stage):
raise ValidationException("Proposal stage {} is not a valid stage".format(stage))
if category and not Category.includes(category):
@@ -294,37 +293,56 @@ class Proposal(db.Model):
raise ValidationException("Only the first milestone can have an immediate payout")
if len(milestone.title) > 60:
- raise ValidationException("Milestone title must be no more than 60 chars")
+ raise ValidationException("Milestone title cannot be longer than 60 chars")
if len(milestone.content) > 200:
- raise ValidationException("Milestone content must be no more than 200 chars")
+ raise ValidationException("Milestone content cannot be longer than 200 chars")
payout_total += float(milestone.payout_percent)
try:
present = datetime.datetime.today().replace(day=1)
if present > milestone.date_estimated:
- raise ValidationException("Milestone date_estimated must be in the future ")
+ raise ValidationException("Milestone date estimate must be in the future ")
except Exception as e:
current_app.logger.warn(
f"Unexpected validation error - client prohibits {e}"
)
- raise ValidationException("date_estimated does not convert to a datetime")
+ raise ValidationException("Date estimate is not a valid datetime")
if payout_total != 100.0:
- raise ValidationException("payoutPercent across milestones must sum to exactly 100")
+ raise ValidationException("Payout percentages of milestones must add up to exactly 100%")
def validate_publishable(self):
self.validate_publishable_milestones()
# Require certain fields
-
required_fields = ['title', 'content', 'brief', 'category', 'target', 'payout_address']
for field in required_fields:
if not hasattr(self, field):
raise ValidationException("Proposal must have a {}".format(field))
+ # Stricter limits on certain fields
+ title = proposal.get('title')
+ brief = proposal.get('brief')
+ content = proposal.get('content')
+ target = proposal.get('target')
+ deadline_duration = proposal.get('deadline_duration')
+
+ if len(title) > 60:
+ raise ValidationException("Proposal title cannot be longer than 60 characters")
+ if len(brief) > 140:
+ raise ValidationException("Brief cannot be longer than 140 characters")
+ if len(content) > 250000:
+ raise ValidationException("Content cannot be longer than 250,000 characters")
+ if Decimal(target) > PROPOSAL_TARGET_MAX:
+ raise ValidationException("Target cannot be more than {} ZEC".format(PROPOSAL_TARGET_MAX))
+ if Decimal(target) < 0.0001:
+ raise ValidationException("Target cannot be less than 0.0001")
+ if deadline_duration > 7776000:
+ raise ValidationException("Deadline duration cannot be more than 90 days")
+
# Check with node that the address is kosher
try:
res = blockchain_get('/validate/address', {'address': self.payout_address})
@@ -380,12 +398,12 @@ class Proposal(db.Model):
payout_address: str = '',
deadline_duration: int = 5184000 # 60 days
):
- self.title = title
- self.brief = brief
+ self.title = title[:255]
+ self.brief = brief[:255]
self.category = category
- self.content = content
- self.target = target if target != '' else None
- self.payout_address = payout_address
+ self.content = content[:300000]
+ self.target = target[:255] if target != '' else None
+ self.payout_address = payout_address[:255]
self.deadline_duration = deadline_duration
Proposal.simple_validate(vars(self))
diff --git a/backend/grant/proposal/views.py b/backend/grant/proposal/views.py
index 3893a98c..ecd5a454 100644
--- a/backend/grant/proposal/views.py
+++ b/backend/grant/proposal/views.py
@@ -218,6 +218,7 @@ def get_proposal_drafts():
@blueprint.route("/", methods=["PUT"])
@requires_team_member_auth
@body({
+ # Length checks are to prevent database errors, not actual user limits imposed
"title": fields.Str(required=True),
"brief": fields.Str(required=True),
"category": fields.Str(required=True, validate=validate.OneOf(choices=Category.list() + [''])),
@@ -226,7 +227,7 @@ def get_proposal_drafts():
"payoutAddress": fields.Str(required=True),
"deadlineDuration": fields.Int(required=True),
"milestones": fields.List(fields.Dict(), required=True),
- "rfpOptIn": fields.Bool(required=False, missing=None)
+ "rfpOptIn": fields.Bool(required=False, missing=None),
})
def update_proposal(milestones, proposal_id, rfp_opt_in, **kwargs):
# Update the base proposal fields
diff --git a/backend/grant/settings.py b/backend/grant/settings.py
index 7aa6780a..d82b013a 100644
--- a/backend/grant/settings.py
+++ b/backend/grant/settings.py
@@ -61,6 +61,7 @@ BLOCKCHAIN_API_SECRET = env.str("BLOCKCHAIN_API_SECRET")
EXPLORER_URL = env.str("EXPLORER_URL", default="https://chain.so/tx/ZECTEST/")
PROPOSAL_STAKING_AMOUNT = Decimal(env.str("PROPOSAL_STAKING_AMOUNT"))
+PROPOSAL_TARGET_MAX = Decimal(env.str("PROPOSAL_TARGET_MAX"))
UI = {
'NAME': 'ZF Grants',
diff --git a/frontend/.env.example b/frontend/.env.example
index 186f9d4f..83333a15 100644
--- a/frontend/.env.example
+++ b/frontend/.env.example
@@ -16,7 +16,7 @@ BACKEND_URL=http://localhost:5000
# EXPLORER_URL="https://chain.so/tx/ZEC/"
EXPLORER_URL="https://chain.so/tx/ZECTEST/"
-# Amount for staking a proposal in ZEC
+# Amount for staking a proposal in ZEC, keep in sync with backend .env
PROPOSAL_STAKING_AMOUNT=0.025
# Normally production runs with SSL, this disables that
@@ -24,3 +24,6 @@ DISABLE_SSL=true
# Uncomment if running on testnet
# TESTNET=true
+
+# Maximum amount for a proposal target, keep in sync with backend .env
+PROPOSAL_TARGET_MAX=10000
diff --git a/frontend/client/components/CreateFlow/Basics.tsx b/frontend/client/components/CreateFlow/Basics.tsx
index ab064d83..017988e3 100644
--- a/frontend/client/components/CreateFlow/Basics.tsx
+++ b/frontend/client/components/CreateFlow/Basics.tsx
@@ -147,6 +147,7 @@ class CreateFlowBasics extends React.Component {
type="text"
value={title}
onChange={this.handleInputChange}
+ maxLength={200}
/>
@@ -161,6 +162,7 @@ class CreateFlowBasics extends React.Component {
value={brief}
onChange={this.handleInputChange}
rows={3}
+ maxLength={200}
/>
@@ -196,6 +198,7 @@ class CreateFlowBasics extends React.Component {
value={target}
onChange={this.handleInputChange}
addonAfter="ZEC"
+ maxLength={20}
/>
diff --git a/frontend/client/components/CreateFlow/Details.tsx b/frontend/client/components/CreateFlow/Details.tsx
index ebd145bb..070e504b 100644
--- a/frontend/client/components/CreateFlow/Details.tsx
+++ b/frontend/client/components/CreateFlow/Details.tsx
@@ -1,7 +1,8 @@
import React from 'react';
-import { Form } from 'antd';
+import { Form, Alert } from 'antd';
import MarkdownEditor from 'components/MarkdownEditor';
import { ProposalDraft } from 'types';
+import { getCreateErrors } from 'modules/create/utils';
interface State {
content: string;
@@ -22,6 +23,8 @@ export default class CreateFlowTeam extends React.Component {
}
render() {
+ const errors = getCreateErrors(this.state, true);
+
return (
);
}
diff --git a/frontend/client/components/CreateFlow/index.less b/frontend/client/components/CreateFlow/index.less
index 3ffec37e..9d28fa23 100644
--- a/frontend/client/components/CreateFlow/index.less
+++ b/frontend/client/components/CreateFlow/index.less
@@ -138,6 +138,10 @@
font-size: 0.8rem;
opacity: 0.3;
animation: draft-notification-popup 120ms ease 1;
+
+ &.is-error {
+ color: @error-color;
+ }
}
&-loading {
diff --git a/frontend/client/components/CreateFlow/index.tsx b/frontend/client/components/CreateFlow/index.tsx
index 8de21124..2c4142ea 100644
--- a/frontend/client/components/CreateFlow/index.tsx
+++ b/frontend/client/components/CreateFlow/index.tsx
@@ -103,6 +103,7 @@ interface StateProps {
form: AppState['create']['form'];
isSavingDraft: AppState['create']['isSavingDraft'];
hasSavedDraft: AppState['create']['hasSavedDraft'];
+ saveDraftError: AppState['create']['saveDraftError'];
}
interface DispatchProps {
@@ -149,7 +150,7 @@ class CreateFlow extends React.Component {
}
render() {
- const { isSavingDraft } = this.props;
+ const { isSavingDraft, saveDraftError } = this.props;
const { step, isPreviewing, isSubmitting, isShowingSubmitWarning } = this.state;
const info = STEP_INFO[step];
@@ -238,8 +239,16 @@ class CreateFlow extends React.Component {
)}
)}
- {isSavingDraft && (
+ {isSavingDraft ? (
Saving draft...
+ ) : (
+ saveDraftError && (
+
+ Failed to save draft!
+
+ {saveDraftError}
+
+ )
)}
{
}
const withConnect = connect(
- (state: AppState) => {
- return {
- form: state.create.form,
- isSavingDraft: state.create.isSavingDraft,
- hasSavedDraft: state.create.hasSavedDraft,
- };
- },
+ (state: AppState) => ({
+ form: state.create.form,
+ isSavingDraft: state.create.isSavingDraft,
+ hasSavedDraft: state.create.hasSavedDraft,
+ saveDraftError: state.create.saveDraftError,
+ }),
{
updateForm: createActions.updateForm,
},
diff --git a/frontend/client/modules/create/utils.ts b/frontend/client/modules/create/utils.ts
index 123f6343..f2963f40 100644
--- a/frontend/client/modules/create/utils.ts
+++ b/frontend/client/modules/create/utils.ts
@@ -13,8 +13,6 @@ import {
PROPOSAL_DETAIL_INITIAL_STATE,
} from 'modules/proposals/reducers';
-export const TARGET_ZEC_LIMIT = 1000;
-
interface CreateFormErrors {
rfpOptIn?: string;
title?: string;
@@ -57,7 +55,17 @@ export function getCreateErrors(
skipRequired?: boolean,
): CreateFormErrors {
const errors: CreateFormErrors = {};
- const { title, team, milestones, target, payoutAddress, rfp, rfpOptIn, brief } = form;
+ const {
+ title,
+ content,
+ team,
+ milestones,
+ target,
+ payoutAddress,
+ rfp,
+ rfpOptIn,
+ brief,
+ } = form;
// Required fields with no extra validation
if (!skipRequired) {
@@ -90,10 +98,16 @@ export function getCreateErrors(
errors.brief = 'Brief can only be 140 characters maximum';
}
+ // Content limit for our database's sake
+ if (content && content.length > 250000) {
+ errors.content = 'Details can only be 250,000 characters maximum';
+ }
+
// Amount to raise
const targetFloat = target ? parseFloat(target) : 0;
if (target && !Number.isNaN(targetFloat)) {
- const targetErr = getAmountError(targetFloat, TARGET_ZEC_LIMIT);
+ const limit = parseFloat(process.env.PROPOSAL_TARGET_MAX as string);
+ const targetErr = getAmountError(targetFloat, limit);
if (targetErr) {
errors.target = targetErr;
}
diff --git a/frontend/config/env.js b/frontend/config/env.js
index 55017af3..a88cafec 100644
--- a/frontend/config/env.js
+++ b/frontend/config/env.js
@@ -61,6 +61,7 @@ module.exports = () => {
SENTRY_DSN: process.env.SENTRY_DSN || null,
SENTRY_RELEASE: process.env.SENTRY_RELEASE || undefined,
TESTNET: process.env.TESTNET || false,
+ PROPOSAL_TARGET_MAX: process.env.PROPOSAL_TARGET_MAX || '10000',
};
// Stringify all values so we can feed into Webpack DefinePlugin
From 0ce35629a2f414aa0780e0b26e7e3cb1ccc625f4 Mon Sep 17 00:00:00 2001
From: Will O'Beirne
Date: Mon, 18 Mar 2019 14:47:46 -0400
Subject: [PATCH 09/26] More component maxlengths
---
frontend/client/components/CreateFlow/Basics.tsx | 2 +-
frontend/client/components/CreateFlow/Milestones.tsx | 3 +++
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/frontend/client/components/CreateFlow/Basics.tsx b/frontend/client/components/CreateFlow/Basics.tsx
index 017988e3..2abbf5cf 100644
--- a/frontend/client/components/CreateFlow/Basics.tsx
+++ b/frontend/client/components/CreateFlow/Basics.tsx
@@ -198,7 +198,7 @@ class CreateFlowBasics extends React.Component {
value={target}
onChange={this.handleInputChange}
addonAfter="ZEC"
- maxLength={20}
+ maxLength={16}
/>
diff --git a/frontend/client/components/CreateFlow/Milestones.tsx b/frontend/client/components/CreateFlow/Milestones.tsx
index 15b830eb..2766a739 100644
--- a/frontend/client/components/CreateFlow/Milestones.tsx
+++ b/frontend/client/components/CreateFlow/Milestones.tsx
@@ -124,6 +124,7 @@ const MilestoneFields = ({
name="title"
value={milestone.title}
onChange={ev => onChange(index, { ...milestone, title: ev.currentTarget.value })}
+ maxLength={80}
/>
diff --git a/frontend/client/static/locales/en/common.json b/frontend/client/static/locales/en/common.json
index ceffcaf0..430d808a 100644
--- a/frontend/client/static/locales/en/common.json
+++ b/frontend/client/static/locales/en/common.json
@@ -16,7 +16,7 @@
"requests": {
"title": "Open Requests from the ZF",
- "description": "The Zcash Foundation will periodically open up requests for proposals that have financial incentives attached to them in the form of fixed bounties, or pledges of contribution matching.\nProposals will be reviewed and chosen based on the ZF’s confidence in the team and their plan.\nTo be eligible for funding from the Zcash Foundation, teams must provide identifying information for legal reasons.",
+ "description": "The Zcash Foundation will periodically open up requests for proposals that have financial incentives attached to them in the form of fixed bounties, or pledges of contribution matching.\nProposals will be reviewed and chosen based on the ZF’s confidence in the team and their plan.\nTo be eligible for funding from the Zcash Foundation, teams must provide identifying information for legal reasons.\nIf none of the RFPs catch your eye, check out the list of promising ideas<\/a>!",
"more": "See all requests",
"emptyTitle": "No open requests at this time",
"emptySubtitle": "But don’t let that stop you! Proposals can be submitted at any time."
From 339705207a8d8f3eb0e2b078adeaf66c4a6063b0 Mon Sep 17 00:00:00 2001
From: William O'Beirne
Date: Thu, 28 Mar 2019 13:25:34 -0400
Subject: [PATCH 26/26] Default target to '0' (#413)
* Default proposal target to zero instead of None
* Add frontend validation and 1 ZEC minimum
---
backend/grant/proposal/models.py | 2 +-
frontend/client/components/CreateFlow/Basics.tsx | 6 ++++++
frontend/client/modules/create/utils.ts | 2 +-
frontend/client/utils/validators.ts | 4 +++-
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/backend/grant/proposal/models.py b/backend/grant/proposal/models.py
index 050a268e..84f610fa 100644
--- a/backend/grant/proposal/models.py
+++ b/backend/grant/proposal/models.py
@@ -405,7 +405,7 @@ class Proposal(db.Model):
self.brief = brief[:255]
self.category = category
self.content = content[:300000]
- self.target = target[:255] if target != '' else None
+ self.target = target[:255] if target != '' else '0'
self.payout_address = payout_address[:255]
self.deadline_duration = deadline_duration
Proposal.simple_validate(vars(self))
diff --git a/frontend/client/components/CreateFlow/Basics.tsx b/frontend/client/components/CreateFlow/Basics.tsx
index 2abbf5cf..cc205969 100644
--- a/frontend/client/components/CreateFlow/Basics.tsx
+++ b/frontend/client/components/CreateFlow/Basics.tsx
@@ -68,6 +68,12 @@ class CreateFlowBasics extends React.Component {
const { title, brief, category, target, rfp, rfpOptIn } = this.state;
const errors = getCreateErrors(this.state, true);
+ // Don't show target error at zero since it defaults to that
+ // Error just shows up at the end to prevent submission
+ if (target === '0') {
+ errors.target = undefined;
+ }
+
const rfpOptInRequired =
rfp && (rfp.matching || (rfp.bounty && new BN(rfp.bounty).gtn(0)));
diff --git a/frontend/client/modules/create/utils.ts b/frontend/client/modules/create/utils.ts
index bbcd51eb..8ef9a107 100644
--- a/frontend/client/modules/create/utils.ts
+++ b/frontend/client/modules/create/utils.ts
@@ -107,7 +107,7 @@ export function getCreateErrors(
const targetFloat = target ? parseFloat(target) : 0;
if (target && !Number.isNaN(targetFloat)) {
const limit = parseFloat(process.env.PROPOSAL_TARGET_MAX as string);
- const targetErr = getAmountError(targetFloat, limit);
+ const targetErr = getAmountError(targetFloat, limit, 1);
if (targetErr) {
errors.target = targetErr;
}
diff --git a/frontend/client/utils/validators.ts b/frontend/client/utils/validators.ts
index e7cd7957..0fa80bf4 100644
--- a/frontend/client/utils/validators.ts
+++ b/frontend/client/utils/validators.ts
@@ -1,4 +1,4 @@
-export function getAmountError(amount: number, max: number = Infinity) {
+export function getAmountError(amount: number, max: number = Infinity, min?: number) {
if (amount < 0) {
return 'Amount must be a positive number';
} else if (
@@ -8,6 +8,8 @@ export function getAmountError(amount: number, max: number = Infinity) {
return 'Must be in increments of 0.001';
} else if (amount > max) {
return `Cannot exceed maximum (${max} ZEC)`;
+ } else if (min && amount < min) {
+ return `Must be at least ${min} ZEC`;
}
return null;