zcash-grant-system/backend/grant/milestone/models.py

228 lines
9.0 KiB
Python
Raw Normal View History

2018-09-10 09:55:26 -07:00
import datetime
from grant.extensions import ma, db
2019-03-14 13:29:02 -07:00
from grant.utils.enums import MilestoneStage
2018-11-13 08:07:09 -08:00
from grant.utils.exceptions import ValidationException
2019-02-11 13:08:51 -08:00
from grant.utils.ma_fields import UnixDate
from grant.utils.misc import gen_random_id
from grant.task.jobs import MilestoneDeadline
2018-09-10 09:55:26 -07:00
2019-02-11 13:08:51 -08:00
class MilestoneException(Exception):
pass
2018-09-10 09:55:26 -07:00
class Milestone(db.Model):
__tablename__ = "milestone"
id = db.Column(db.Integer(), primary_key=True)
2019-02-11 13:08:51 -08:00
index = db.Column(db.Integer(), nullable=False)
2018-09-10 09:55:26 -07:00
date_created = db.Column(db.DateTime, nullable=False)
title = db.Column(db.String(255), nullable=False)
content = db.Column(db.Text, nullable=False)
payout_percent = db.Column(db.String(255), nullable=False)
immediate_payout = db.Column(db.Boolean)
date_estimated = db.Column(db.DateTime, nullable=True)
days_estimated = db.Column(db.String(255), nullable=True)
2018-09-10 09:55:26 -07:00
2019-02-11 13:08:51 -08:00
stage = db.Column(db.String(255), nullable=False)
date_requested = db.Column(db.DateTime, nullable=True)
requested_user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
date_rejected = db.Column(db.DateTime, nullable=True)
reject_reason = db.Column(db.String(255))
reject_arbiter_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
date_accepted = db.Column(db.DateTime, nullable=True)
accept_arbiter_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
date_paid = db.Column(db.DateTime, nullable=True)
paid_tx_id = db.Column(db.String(255))
2018-09-10 09:55:26 -07:00
proposal_id = db.Column(db.Integer, db.ForeignKey("proposal.id"), nullable=False)
def __init__(
self,
2019-02-11 13:08:51 -08:00
index: int,
2018-09-10 09:55:26 -07:00
title: str,
content: str,
days_estimated: str,
2018-09-10 09:55:26 -07:00
payout_percent: str,
immediate_payout: bool,
2019-02-11 13:08:51 -08:00
stage: str = MilestoneStage.IDLE,
proposal_id=int,
2018-09-10 09:55:26 -07:00
):
self.id = gen_random_id(Milestone)
2019-03-18 12:11:24 -07:00
self.title = title[:255]
self.content = content[:255]
2018-09-10 09:55:26 -07:00
self.stage = stage
self.days_estimated = days_estimated[:255]
2019-03-18 12:11:24 -07:00
self.payout_percent = payout_percent[:255]
2018-09-10 09:55:26 -07:00
self.immediate_payout = immediate_payout
self.proposal_id = proposal_id
self.date_created = datetime.datetime.now()
2019-02-11 13:08:51 -08:00
self.index = index
2019-01-23 07:00:30 -08:00
2019-03-14 13:29:02 -07:00
@staticmethod
def make(milestones_data, proposal):
if milestones_data:
# Delete & re-add milestones
[db.session.delete(x) for x in proposal.milestones]
for i, milestone_data in enumerate(milestones_data):
m = Milestone(
title=milestone_data["title"][:255],
content=milestone_data["content"][:255],
days_estimated=str(milestone_data["days_estimated"])[:255],
payout_percent=str(milestone_data["payout_percent"])[:255],
2019-03-14 13:29:02 -07:00
immediate_payout=milestone_data["immediate_payout"],
proposal_id=proposal.id,
index=i
)
db.session.add(m)
ZF Grants 2.1 (#496) * fix ccr pagination defaults * add ccr admin tests * add ccr user tests * checkpoint * fix tslint * request changes discussion flow mvp * admin - add discussion status * backend - add live drafts * admin - add live drafts * frontend - add live drafts * frontend - add edit discussion proposal * fix tsc * include DISCUSSION status in propsal listview * do not make live draft on admin request changes * hide live drafts from user proposal draft list * fix backend tests * add admin tests * add user tests * fix: liking, viewing discussion proposals, admin menu * admin - update hints for live drafts * fe - add better messaging when updating a proposal * be - fix like test * remove TODO comments * add new email types * fix storybook * add revision tab story * backend - implement proposal revisions * frontend - implement proposal revisions * update revision tab story * fix lint * remove set detection * email proposal followers on revision * restrict banner to team members only * misc bug fixes * update, add backend tests * add milestone title change to revision history story * fix milestones display in preview * allow archived proposals to be queried * implement archived proposal page * fix tsc * implement archived proposal get route * move styling into less * remove proposal archive parent id * handle archived proposal status * cleanup * remove contributions, switch to USD, implement quarters * use Qs to preserve formatting * handle edit only kyc * prevent ARCHIVED proposals from being sent to admin * display latest revision first * admin - proposal & ccr reject permanently * backend - proposal & ccr reject permanently * frontend - proposal & ccr reject permanently * fix tsc * use $ in milestone payout email * introduce custom filters to proposal listview * hide archive link on first revision * upgrade packages * add bech32 implementation * add z address validation with tests * fix tslint * use local address validation * fix tests, remove blockchain mock gets * add additional bad addresses * update briefs to include page break message * remove contributions routes, menu entry * disable countribution count admin stats * remove matching and pretty print in finance * fix tslint * separate out rejected permanently proposals * make removing proposals generic * allow linked tabs to be ignored * remove rejected permanently, bugfix * update preview link to point to rejected tab * implement rejected permanently tab, add tab message * refactor variable * fix tslint * fix tslint * send ccr reject permanently email on rejection * fix preview message * wire up proposal arbiter and rejected emails * disable tip jar in proposal and profile * sync ccr/proposal drafts on create form init * check invites on submit modal open * update team invite language * update team text when edit * fix ccr rejected permanently tag * text changes, email preview fix * display changes requested tag when in discussion with changes requested * enable social share on open for discussion proposals, update language * place sort below filter * derive filter from query string * use better filter names in query params * fix tslint * create snapshot of original proposal on first revision * clear invites between edits, account for additional changes not tracked in revisions * update tests * fix test * remove print * SameSite Fixes (#150) * QA Fixes 2 (#151) * set filters as query strings on change * remove rejected permanently tags * add dollar sign in financials legend * fix tsc * Copy Touchups (#152) * Email Fixes (#155) * fix ZEC in milestone payout emails * fix links in rejected permanently CCR/proposal emails * Poll for Team and Invite Changes in Create Flow (#153) * poll for team and invite changes in create flow * fix tslint Co-authored-by: Daniel Ternyak <dternyak@gmail.com> * pretty print payouts by quarter (#156) Co-authored-by: Daniel Ternyak <dternyak@gmail.com> * Remove Blockchain Module (#154) * remove blockchain route from backend, remove calls to node * revert blockchain_get removal * Add Tags to Proposal Cards (#157) * add tag to proposals and dynamically set v1 card height * listen on window resize * make card height props optional * set tag in bottom right, remove dynamic card resize, add dynamic tag resize * cleanup * cleanup Co-authored-by: Daniel Ternyak <dternyak@gmail.com> * Improve Frontend Address Validation (#158) Co-authored-by: Daniel Ternyak <dternyak@gmail.com> * Remove blockchain module (#162) * remove blockchain route from backend, remove calls to node * revert blockchain_get removal * Remove Blockchain App (#160) * remove blockchain app * remove blockchain app from travis Co-authored-by: Danny Skubak <skubakdj@gmail.com> * Proposal Edit Fixes (#161) * fe - display error if edit creation fails * be - restrict live draft publish Co-authored-by: Daniel Ternyak <dternyak@gmail.com> * Restrict Arbiter Assignment (#159) Co-authored-by: Daniel Ternyak <dternyak@gmail.com> * Email Copy updates * Remove Admin Financials Card * Hookup 'proposal_approved_without_funding' to admin email example * bump various package versions * Update yarn.lock files * Attach 'proposal_approved_without_funding' to backend example email * bump package versions Co-authored-by: Danny Skubak <skubakdj@gmail.com>
2020-04-07 19:56:32 -07:00
# clone milestones from one proposal to another
@staticmethod
def clone(source_proposal, destination_proposal):
# delete any milestones on destination proposal
[db.session.delete(ms) for ms in destination_proposal.milestones]
# copy milestones from source proposal to destination proposal
for i, ms in enumerate(source_proposal.milestones):
new_ms = Milestone(
proposal_id=destination_proposal.id,
title=ms.title,
content=ms.content,
days_estimated=ms.days_estimated,
payout_percent=ms.payout_percent,
immediate_payout=ms.immediate_payout,
index=i
)
db.session.add(new_ms)
# The purpose of this method is to set the `date_estimated` property on all milestones in a proposal. This works
# by figuring out a starting point for each milestone (the `base_date` below) and adding `days_estimated`.
#
# As proposal creators now estimate their milestones in days (instead of picking months), this method allows us to
# keep `date_estimated` in sync throughout the lifecycle of a proposal. For example, if a user misses their
# first milestone deadline by a week, this method would take the actual completion date of that milestone and
# adjust the `date_estimated` of the remaining milestones accordingly.
#
@staticmethod
def set_v2_date_estimates(proposal):
if not proposal.date_approved:
raise MilestoneException(f'Cannot estimate milestone dates because proposal has no date_approved set')
# The milestone being actively worked on
current_milestone = proposal.current_milestone
if current_milestone.stage == MilestoneStage.PAID:
raise MilestoneException(f'Cannot estimate milestone dates because they are all completed')
# The starting point for `date_estimated` calculation for each uncompleted milestone
# We add `days_estimated` to `base_date` to calculate `date_estimated`
base_date = None
for index, milestone in enumerate(proposal.milestones):
if index == 0:
# If it's the first milestone, use the proposal approval date as a `base_date`
base_date = proposal.date_approved
if milestone.date_paid:
# If milestone has been paid, set `base_date` for the next milestone and noop out
base_date = milestone.date_paid
continue
days_estimated = milestone.days_estimated if not milestone.immediate_payout else "0"
date_estimated = base_date + datetime.timedelta(days=int(days_estimated))
milestone.date_estimated = date_estimated
# Set the `base_date` for the next milestone using the estimate completion date of the current milestone
base_date = date_estimated
db.session.add(milestone)
# Skip task creation if current milestone has an immediate payout
if current_milestone.immediate_payout:
return
# Create MilestoneDeadline task for the current milestone so arbiters will be alerted if the deadline is missed
task = MilestoneDeadline(proposal, current_milestone)
task.make_task()
2019-02-11 13:08:51 -08:00
def request_payout(self, user_id: int):
if self.stage not in [MilestoneStage.IDLE, MilestoneStage.REJECTED]:
2019-02-11 13:08:51 -08:00
raise MilestoneException(f'Cannot request payout for milestone at {self.stage} stage')
self.stage = MilestoneStage.REQUESTED
self.date_requested = datetime.datetime.now()
self.requested_user_id = user_id
def reject_request(self, arbiter_id: int, reason: str):
if self.stage != MilestoneStage.REQUESTED:
raise MilestoneException(f'Cannot reject payout request for milestone at {self.stage} stage')
self.stage = MilestoneStage.REJECTED
self.date_rejected = datetime.datetime.now()
self.reject_reason = reason
self.reject_arbiter_id = arbiter_id
def accept_immediate(self):
if self.immediate_payout and self.index == 0:
2019-04-16 10:38:14 -07:00
self.proposal.send_admin_email('admin_payout')
self.date_requested = datetime.datetime.now()
self.stage = MilestoneStage.ACCEPTED
self.date_accepted = datetime.datetime.now()
db.session.add(self)
db.session.flush()
2019-02-11 13:08:51 -08:00
def accept_request(self, arbiter_id: int):
if self.stage != MilestoneStage.REQUESTED:
raise MilestoneException(f'Cannot accept payout request for milestone at {self.stage} stage')
2019-04-16 10:38:14 -07:00
self.proposal.send_admin_email('admin_payout')
2019-02-13 08:54:46 -08:00
self.stage = MilestoneStage.ACCEPTED
2019-02-11 13:08:51 -08:00
self.date_accepted = datetime.datetime.now()
self.accept_arbiter_id = arbiter_id
def mark_paid(self, tx_id: str):
if self.stage != MilestoneStage.ACCEPTED:
raise MilestoneException(f'Cannot pay a milestone at {self.stage} stage')
self.stage = MilestoneStage.PAID
self.date_paid = datetime.datetime.now()
self.paid_tx_id = tx_id
2018-09-10 09:55:26 -07:00
class MilestoneSchema(ma.Schema):
class Meta:
model = Milestone
# Fields to expose
fields = (
"title",
2019-02-11 13:08:51 -08:00
"index",
"id",
2018-09-10 09:55:26 -07:00
"content",
"stage",
"payout_percent",
"immediate_payout",
2019-02-11 13:08:51 -08:00
"reject_reason",
"paid_tx_id",
2018-09-10 09:55:26 -07:00
"date_created",
2019-02-11 13:08:51 -08:00
"date_estimated",
"date_requested",
"date_rejected",
"date_accepted",
"date_paid",
"days_estimated"
2018-09-10 09:55:26 -07:00
)
2019-02-11 13:08:51 -08:00
date_created = UnixDate(attribute='date_created')
date_estimated = UnixDate(attribute='date_estimated')
date_requested = UnixDate(attribute='date_requested')
date_rejected = UnixDate(attribute='date_rejected')
date_accepted = UnixDate(attribute='date_accepted')
date_paid = UnixDate(attribute='date_paid')
2018-09-10 09:55:26 -07:00
milestone_schema = MilestoneSchema()
milestones_schema = MilestoneSchema(many=True)