From 36c150afcd74e01ddf03347f687ffddb62733a2c Mon Sep 17 00:00:00 2001 From: Daniel Ternyak Date: Wed, 24 Jul 2019 13:29:11 -0500 Subject: [PATCH] Proposal milestone date validation --- backend/grant/proposal/models.py | 20 +++---- blockchain/.gitignore | 1 + .../components/CreateFlow/Milestones.tsx | 4 +- frontend/client/modules/create/utils.ts | 58 +++++++++++++------ 4 files changed, 54 insertions(+), 29 deletions(-) diff --git a/backend/grant/proposal/models.py b/backend/grant/proposal/models.py index ea9d4c91..2ca822de 100644 --- a/backend/grant/proposal/models.py +++ b/backend/grant/proposal/models.py @@ -309,17 +309,6 @@ class Proposal(db.Model): payout_total += p - try: - present = datetime.datetime.today().replace(day=1) - if present > milestone.date_estimated: - 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 estimate is not a valid datetime") - if payout_total != 100.0: raise ValidationException("Payout percentages of milestones must add up to exactly 100%") @@ -358,6 +347,14 @@ class Proposal(db.Model): # Then run through regular validation Proposal.simple_validate(vars(self)) + # only do this when user submits for approval, there is a chance the dates will + # be passed by the time admin approval / user publishing occurs + def validate_milestone_dates(self): + present = datetime.datetime.today().replace(day=1, hour=0, minute=0, second=0, microsecond=0) + for milestone in self.milestones: + if present > milestone.date_estimated: + raise ValidationException("Milestone date estimate must be in the future ") + @staticmethod def create(**kwargs): Proposal.simple_validate(kwargs) @@ -475,6 +472,7 @@ class Proposal(db.Model): # state: status (DRAFT || REJECTED) -> (PENDING || STAKING) def submit_for_approval(self): self.validate_publishable() + self.validate_milestone_dates() allowed_statuses = [ProposalStatus.DRAFT, ProposalStatus.REJECTED] # specific validation if self.status not in allowed_statuses: diff --git a/blockchain/.gitignore b/blockchain/.gitignore index 004ac4f2..f892b34e 100755 --- a/blockchain/.gitignore +++ b/blockchain/.gitignore @@ -56,6 +56,7 @@ typings/ # dotenv environment variables file .env +.env.testnet # next.js build output .next diff --git a/frontend/client/components/CreateFlow/Milestones.tsx b/frontend/client/components/CreateFlow/Milestones.tsx index 2766a739..2e060621 100644 --- a/frontend/client/components/CreateFlow/Milestones.tsx +++ b/frontend/client/components/CreateFlow/Milestones.tsx @@ -161,7 +161,9 @@ const MilestoneFields = ({ } format="MMMM YYYY" allowClear={false} - onChange={time => onChange(index, { ...milestone, dateEstimated: time.unix() })} + onChange={time => + onChange(index, { ...milestone, dateEstimated: time.startOf('month').unix() }) + } disabled={milestone.immediatePayout} disabledDate={current => { if (!previousMilestoneDateEstimate) { diff --git a/frontend/client/modules/create/utils.ts b/frontend/client/modules/create/utils.ts index ed0b08d1..f10ee577 100644 --- a/frontend/client/modules/create/utils.ts +++ b/frontend/client/modules/create/utils.ts @@ -1,4 +1,11 @@ -import { ProposalDraft, STATUS, MILESTONE_STAGE, PROPOSAL_ARBITER_STATUS } from 'types'; +import { + ProposalDraft, + STATUS, + MILESTONE_STAGE, + PROPOSAL_ARBITER_STATUS, + CreateMilestone, +} from 'types'; +import moment from 'moment'; import { User } from 'types'; import { getAmountError, @@ -127,23 +134,9 @@ export function getCreateErrors( // Milestones if (milestones) { let cumulativeMilestonePct = 0; + let lastMsEst: CreateMilestone['dateEstimated'] = 0; const milestoneErrors = milestones.map((ms, idx) => { - if (!ms.title) { - return 'Title is required'; - } else if (ms.title.length > 40) { - return 'Title length can only be 40 characters maximum'; - } - - if (!ms.content) { - return 'Description is required'; - } else if (ms.content.length > 200) { - return 'Description can only be 200 characters maximum'; - } - - if (!ms.dateEstimated) { - return 'Estimate date is required'; - } - + // check payout first so we collect the cumulativePayout even if other fields are invalid if (!ms.payoutPercent) { return 'Payout percent is required'; } else if (Number.isNaN(parseInt(ms.payoutPercent, 10))) { @@ -158,6 +151,37 @@ export function getCreateErrors( // Last one shows percentage errors cumulativeMilestonePct += parseInt(ms.payoutPercent, 10); + + if (!ms.title) { + return 'Title is required'; + } else if (ms.title.length > 40) { + return 'Title length can only be 40 characters maximum'; + } + + if (!ms.content) { + return 'Description is required'; + } else if (ms.content.length > 200) { + return 'Description can only be 200 characters maximum'; + } + + if (!ms.dateEstimated) { + return 'Estimate date is required'; + } else { + // FE validation on milestone estimation + if ( + ms.dateEstimated < + moment(Date.now()) + .startOf('month') + .unix() + ) { + return 'Estimate date should be in the future'; + } + if (ms.dateEstimated <= lastMsEst) { + return 'Estimate date should be later than previous estimate date'; + } + lastMsEst = ms.dateEstimated; + } + if ( idx === milestones.length - 1 && cumulativeMilestonePct !== 100 &&