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
|
2019-03-04 13:47:52 -08:00
|
|
|
from grant.utils.misc import gen_random_id
|
2019-11-13 14:38:17 -08:00
|
|
|
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)
|
2019-11-13 14:38:17 -08:00
|
|
|
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,
|
2019-11-13 14:38:17 -08:00
|
|
|
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
|
|
|
):
|
2019-03-04 13:47:52 -08: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
|
2019-11-13 14:38:17 -08:00
|
|
|
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-11-13 14:38:17 -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(
|
2019-03-18 11:35:08 -07:00
|
|
|
title=milestone_data["title"][:255],
|
|
|
|
content=milestone_data["content"][:255],
|
2019-11-13 14:38:17 -08:00
|
|
|
days_estimated=str(milestone_data["days_estimated"])[:255],
|
2019-03-18 11:35:08 -07:00
|
|
|
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)
|
|
|
|
|
2019-11-13 14:38:17 -08:00
|
|
|
# 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):
|
2019-02-11 21:10:09 -08:00
|
|
|
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
|
|
|
|
|
2019-02-15 19:35:25 -08:00
|
|
|
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')
|
2019-02-15 19:35:25 -08:00
|
|
|
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",
|
2019-11-13 14:38:17 -08:00
|
|
|
"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')
|
2019-02-05 13:25:58 -08:00
|
|
|
|
2018-09-10 09:55:26 -07:00
|
|
|
|
|
|
|
milestone_schema = MilestoneSchema()
|
|
|
|
milestones_schema = MilestoneSchema(many=True)
|