2019-02-13 12:30:58 -08:00
|
|
|
|
from decimal import Decimal
|
2019-10-10 17:12:38 -07:00
|
|
|
|
from datetime import datetime
|
2019-03-01 12:11:03 -08:00
|
|
|
|
|
2019-03-13 12:40:29 -07:00
|
|
|
|
from flask import Blueprint, g, request, current_app
|
2019-03-01 12:11:03 -08:00
|
|
|
|
from marshmallow import fields, validate
|
|
|
|
|
from sqlalchemy import or_
|
2019-03-13 14:39:50 -07:00
|
|
|
|
from sentry_sdk import capture_message
|
2019-03-18 12:03:01 -07:00
|
|
|
|
from webargs import validate
|
2019-03-01 12:11:03 -08:00
|
|
|
|
|
2019-03-12 20:35:38 -07:00
|
|
|
|
from grant.extensions import limiter
|
2018-11-08 10:42:19 -08:00
|
|
|
|
from grant.comment.models import Comment, comment_schema, comments_schema
|
2019-03-14 09:46:09 -07:00
|
|
|
|
from grant.email.send import send_email
|
2019-01-22 21:35:22 -08:00
|
|
|
|
from grant.milestone.models import Milestone
|
2019-03-01 12:11:03 -08:00
|
|
|
|
from grant.parser import body, query, paginated_fields
|
|
|
|
|
from grant.rfp.models import RFP
|
2019-03-14 21:16:38 -07:00
|
|
|
|
from grant.settings import PROPOSAL_STAKING_AMOUNT
|
2019-11-05 11:38:34 -08:00
|
|
|
|
from grant.task.jobs import ProposalDeadline, PruneDraft
|
2019-01-22 21:35:22 -08:00
|
|
|
|
from grant.user.models import User
|
2019-03-01 12:11:03 -08:00
|
|
|
|
from grant.utils import pagination
|
2019-02-01 12:43:43 -08:00
|
|
|
|
from grant.utils.auth import (
|
|
|
|
|
requires_auth,
|
|
|
|
|
requires_team_member_auth,
|
2019-02-11 21:10:09 -08:00
|
|
|
|
requires_arbiter_auth,
|
2019-02-01 12:43:43 -08:00
|
|
|
|
requires_email_verified_auth,
|
|
|
|
|
get_authed_user,
|
|
|
|
|
internal_webhook
|
|
|
|
|
)
|
2019-03-01 12:11:03 -08:00
|
|
|
|
from grant.utils.enums import Category
|
2019-10-10 17:12:38 -07:00
|
|
|
|
from grant.utils.enums import ProposalStatus, ProposalStage, ContributionStatus, RFPStatus
|
2018-11-13 08:07:09 -08:00
|
|
|
|
from grant.utils.exceptions import ValidationException
|
2019-03-14 21:16:38 -07:00
|
|
|
|
from grant.utils.misc import is_email, make_url, from_zat, make_explore_url
|
2019-01-09 13:57:15 -08:00
|
|
|
|
from .models import (
|
2018-11-21 19:18:22 -08:00
|
|
|
|
Proposal,
|
|
|
|
|
proposals_schema,
|
|
|
|
|
proposal_schema,
|
|
|
|
|
ProposalUpdate,
|
|
|
|
|
proposal_update_schema,
|
|
|
|
|
ProposalContribution,
|
|
|
|
|
proposal_contribution_schema,
|
2018-11-26 15:47:24 -08:00
|
|
|
|
proposal_team,
|
2018-11-26 17:14:00 -08:00
|
|
|
|
ProposalTeamInvite,
|
|
|
|
|
proposal_team_invite_schema,
|
2019-01-09 14:54:41 -08:00
|
|
|
|
proposal_proposal_contributions_schema,
|
2019-01-09 10:23:08 -08:00
|
|
|
|
db,
|
2018-11-21 19:18:22 -08:00
|
|
|
|
)
|
2018-09-10 09:55:26 -07:00
|
|
|
|
|
2018-09-18 15:20:17 -07:00
|
|
|
|
blueprint = Blueprint("proposal", __name__, url_prefix="/api/v1/proposals")
|
2018-09-10 09:55:26 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/<proposal_id>", methods=["GET"])
|
|
|
|
|
def get_proposal(proposal_id):
|
2018-11-07 09:33:19 -08:00
|
|
|
|
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
2018-09-10 09:55:26 -07:00
|
|
|
|
if proposal:
|
2019-01-30 09:59:15 -08:00
|
|
|
|
if proposal.status != ProposalStatus.LIVE:
|
|
|
|
|
if proposal.status == ProposalStatus.DELETED:
|
2019-01-09 10:23:08 -08:00
|
|
|
|
return {"message": "Proposal was deleted"}, 404
|
|
|
|
|
authed_user = get_authed_user()
|
|
|
|
|
team_ids = list(x.id for x in proposal.team)
|
|
|
|
|
if not authed_user or authed_user.id not in team_ids:
|
|
|
|
|
return {"message": "User cannot view this proposal"}, 404
|
2018-12-28 15:05:34 -08:00
|
|
|
|
return proposal_schema.dump(proposal)
|
2018-09-10 09:55:26 -07:00
|
|
|
|
else:
|
2018-11-04 10:33:22 -08:00
|
|
|
|
return {"message": "No proposal matching id"}, 404
|
2018-09-10 09:55:26 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/<proposal_id>/comments", methods=["GET"])
|
2019-03-01 12:11:03 -08:00
|
|
|
|
@query(paginated_fields)
|
2019-02-17 18:13:24 -08:00
|
|
|
|
def get_proposal_comments(proposal_id, page, filters, search, sort):
|
|
|
|
|
# only using page, currently
|
|
|
|
|
filters_workaround = request.args.getlist('filters[]')
|
|
|
|
|
page = pagination.comment(
|
|
|
|
|
schema=comments_schema,
|
2019-02-19 13:30:32 -08:00
|
|
|
|
query=Comment.query.filter_by(proposal_id=proposal_id, parent_comment_id=None, hidden=False),
|
2019-02-17 18:13:24 -08:00
|
|
|
|
page=page,
|
|
|
|
|
filters=filters_workaround,
|
|
|
|
|
search=search,
|
|
|
|
|
sort=sort,
|
|
|
|
|
)
|
|
|
|
|
return page
|
2018-09-10 09:55:26 -07:00
|
|
|
|
|
|
|
|
|
|
2019-02-18 14:35:21 -08:00
|
|
|
|
@blueprint.route("/<proposal_id>/comments/<comment_id>/report", methods=["PUT"])
|
|
|
|
|
@requires_email_verified_auth
|
|
|
|
|
def report_proposal_comment(proposal_id, comment_id):
|
|
|
|
|
# Make sure proposal exists
|
2018-11-07 09:33:19 -08:00
|
|
|
|
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
2018-11-08 10:42:19 -08:00
|
|
|
|
if not proposal:
|
2018-11-04 10:33:22 -08:00
|
|
|
|
return {"message": "No proposal matching id"}, 404
|
2018-12-14 11:36:22 -08:00
|
|
|
|
|
2019-02-18 14:35:21 -08:00
|
|
|
|
comment = Comment.query.filter_by(id=comment_id).first()
|
|
|
|
|
if not comment:
|
|
|
|
|
return {"message": "Comment doesn’t exist"}, 404
|
|
|
|
|
|
|
|
|
|
comment.report(True)
|
|
|
|
|
db.session.commit()
|
2019-03-01 12:11:03 -08:00
|
|
|
|
return {"message": "ok"}, 200
|
2018-09-10 09:55:26 -07:00
|
|
|
|
|
|
|
|
|
|
2018-09-18 15:17:34 -07:00
|
|
|
|
@blueprint.route("/<proposal_id>/comments", methods=["POST"])
|
2019-03-12 20:35:38 -07:00
|
|
|
|
@limiter.limit("30/hour;2/minute")
|
2019-02-01 12:43:43 -08:00
|
|
|
|
@requires_email_verified_auth
|
2019-03-01 12:11:03 -08:00
|
|
|
|
@body({
|
2019-06-06 11:20:45 -07:00
|
|
|
|
"comment": fields.Str(required=True, validate=validate.Length(max=5000)),
|
2019-03-01 12:11:03 -08:00
|
|
|
|
"parentCommentId": fields.Int(required=False, missing=None),
|
|
|
|
|
})
|
2018-12-17 10:33:33 -08:00
|
|
|
|
def post_proposal_comments(proposal_id, comment, parent_comment_id):
|
2018-11-08 10:29:29 -08:00
|
|
|
|
# Make sure proposal exists
|
2018-11-07 09:33:19 -08:00
|
|
|
|
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
2018-11-08 10:29:29 -08:00
|
|
|
|
if not proposal:
|
2018-10-22 15:31:33 -07:00
|
|
|
|
return {"message": "No proposal matching id"}, 404
|
2018-09-18 15:17:34 -07:00
|
|
|
|
|
2018-11-08 10:29:29 -08:00
|
|
|
|
# Make sure the parent comment exists
|
2019-01-16 14:26:45 -08:00
|
|
|
|
parent = None
|
2018-11-08 10:29:29 -08:00
|
|
|
|
if parent_comment_id:
|
|
|
|
|
parent = Comment.query.filter_by(id=parent_comment_id).first()
|
|
|
|
|
if not parent:
|
|
|
|
|
return {"message": "Parent comment doesn’t exist"}, 400
|
|
|
|
|
|
2019-01-27 18:51:05 -08:00
|
|
|
|
# Make sure user has verified their email
|
|
|
|
|
if not g.current_user.email_verification.has_verified:
|
2019-02-14 20:11:47 -08:00
|
|
|
|
return {"message": "Please confirm your email before commenting"}, 401
|
|
|
|
|
|
|
|
|
|
# Make sure user is not silenced
|
|
|
|
|
if g.current_user.silenced:
|
2019-02-15 11:09:51 -08:00
|
|
|
|
return {"message": "Your account has been silenced, commenting is disabled."}, 403
|
2019-01-27 18:51:05 -08:00
|
|
|
|
|
2018-11-08 10:29:29 -08:00
|
|
|
|
# Make the comment
|
|
|
|
|
comment = Comment(
|
|
|
|
|
proposal_id=proposal_id,
|
|
|
|
|
user_id=g.current_user.id,
|
|
|
|
|
parent_comment_id=parent_comment_id,
|
|
|
|
|
content=comment
|
|
|
|
|
)
|
|
|
|
|
db.session.add(comment)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
dumped_comment = comment_schema.dump(comment)
|
2019-01-16 14:26:45 -08:00
|
|
|
|
|
2019-03-13 14:39:50 -07:00
|
|
|
|
# Email proposal team if top-level comment
|
2019-01-16 14:26:45 -08:00
|
|
|
|
if not parent:
|
|
|
|
|
for member in proposal.team:
|
|
|
|
|
send_email(member.email_address, 'proposal_comment', {
|
|
|
|
|
'author': g.current_user,
|
|
|
|
|
'proposal': proposal,
|
2019-03-14 18:55:13 -07:00
|
|
|
|
'comment_url': make_url(f'/proposals/{proposal.id}?tab=discussions&comment={comment.id}'),
|
2019-01-16 14:26:45 -08:00
|
|
|
|
'author_url': make_url(f'/profile/{comment.author.id}'),
|
|
|
|
|
})
|
|
|
|
|
# Email parent comment creator, if it's not themselves
|
|
|
|
|
if parent and parent.author.id != comment.author.id:
|
|
|
|
|
send_email(parent.author.email_address, 'comment_reply', {
|
|
|
|
|
'author': g.current_user,
|
|
|
|
|
'proposal': proposal,
|
2019-03-14 18:55:13 -07:00
|
|
|
|
'comment_url': make_url(f'/proposals/{proposal.id}?tab=discussions&comment={comment.id}'),
|
2019-01-16 14:26:45 -08:00
|
|
|
|
'author_url': make_url(f'/profile/{comment.author.id}'),
|
|
|
|
|
})
|
|
|
|
|
|
2018-11-08 10:29:29 -08:00
|
|
|
|
return dumped_comment, 201
|
|
|
|
|
|
2018-09-18 15:17:34 -07:00
|
|
|
|
|
2018-09-10 09:55:26 -07:00
|
|
|
|
@blueprint.route("/", methods=["GET"])
|
2019-03-01 12:11:03 -08:00
|
|
|
|
@query(paginated_fields)
|
2019-02-05 12:34:19 -08:00
|
|
|
|
def get_proposals(page, filters, search, sort):
|
|
|
|
|
filters_workaround = request.args.getlist('filters[]')
|
2019-02-23 13:38:06 -08:00
|
|
|
|
query = Proposal.query.filter_by(status=ProposalStatus.LIVE) \
|
|
|
|
|
.filter(Proposal.stage != ProposalStage.CANCELED) \
|
|
|
|
|
.filter(Proposal.stage != ProposalStage.FAILED)
|
2019-02-05 12:34:19 -08:00
|
|
|
|
page = pagination.proposal(
|
|
|
|
|
schema=proposals_schema,
|
2019-02-23 13:38:06 -08:00
|
|
|
|
query=query,
|
2019-02-05 12:34:19 -08:00
|
|
|
|
page=page,
|
|
|
|
|
filters=filters_workaround,
|
|
|
|
|
search=search,
|
|
|
|
|
sort=sort,
|
|
|
|
|
)
|
|
|
|
|
return page
|
2018-09-10 09:55:26 -07:00
|
|
|
|
|
|
|
|
|
|
2018-11-13 08:07:09 -08:00
|
|
|
|
@blueprint.route("/drafts", methods=["POST"])
|
2019-03-12 20:35:38 -07:00
|
|
|
|
@limiter.limit("10/hour;3/minute")
|
2019-02-01 12:43:43 -08:00
|
|
|
|
@requires_email_verified_auth
|
2019-03-01 12:11:03 -08:00
|
|
|
|
@body({
|
|
|
|
|
"rfpId": fields.Int(required=False, missing=None)
|
|
|
|
|
})
|
2019-02-01 11:13:30 -08:00
|
|
|
|
def make_proposal_draft(rfp_id):
|
2019-01-30 09:59:15 -08:00
|
|
|
|
proposal = Proposal.create(status=ProposalStatus.DRAFT)
|
2018-11-13 08:07:09 -08:00
|
|
|
|
proposal.team.append(g.current_user)
|
2019-02-01 11:13:30 -08:00
|
|
|
|
|
|
|
|
|
if rfp_id:
|
|
|
|
|
rfp = RFP.query.filter_by(id=rfp_id).first()
|
|
|
|
|
if not rfp:
|
|
|
|
|
return {"message": "The request this proposal was made for doesn’t exist"}, 400
|
2019-10-10 17:12:38 -07:00
|
|
|
|
if datetime.now() > rfp.date_closes:
|
|
|
|
|
return {"message": "The request this proposal was made for has expired"}, 400
|
|
|
|
|
if rfp.status == RFPStatus.CLOSED:
|
|
|
|
|
return {"message": "The request this proposal was made for has been closed"}, 400
|
2019-02-01 11:13:30 -08:00
|
|
|
|
rfp.proposals.append(proposal)
|
|
|
|
|
db.session.add(rfp)
|
|
|
|
|
|
2019-11-05 11:38:34 -08:00
|
|
|
|
task = PruneDraft(proposal)
|
|
|
|
|
task.make_task()
|
|
|
|
|
|
2018-11-13 08:07:09 -08:00
|
|
|
|
db.session.add(proposal)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
return proposal_schema.dump(proposal), 201
|
2018-09-10 09:55:26 -07:00
|
|
|
|
|
2018-11-26 15:47:24 -08:00
|
|
|
|
|
2018-11-13 08:07:09 -08:00
|
|
|
|
@blueprint.route("/drafts", methods=["GET"])
|
2018-12-14 11:36:22 -08:00
|
|
|
|
@requires_auth
|
2018-11-13 08:07:09 -08:00
|
|
|
|
def get_proposal_drafts():
|
|
|
|
|
proposals = (
|
|
|
|
|
Proposal.query
|
2019-03-01 12:11:03 -08:00
|
|
|
|
.filter(or_(
|
2019-01-30 09:59:15 -08:00
|
|
|
|
Proposal.status == ProposalStatus.DRAFT,
|
|
|
|
|
Proposal.status == ProposalStatus.REJECTED,
|
|
|
|
|
))
|
2019-03-01 12:11:03 -08:00
|
|
|
|
.join(proposal_team)
|
|
|
|
|
.filter(proposal_team.c.user_id == g.current_user.id)
|
|
|
|
|
.order_by(Proposal.date_created.desc())
|
|
|
|
|
.all()
|
2018-09-10 09:55:26 -07:00
|
|
|
|
)
|
2018-11-13 08:07:09 -08:00
|
|
|
|
return proposals_schema.dump(proposals), 200
|
2018-09-10 09:55:26 -07:00
|
|
|
|
|
2018-12-14 11:36:22 -08:00
|
|
|
|
|
2018-11-13 08:07:09 -08:00
|
|
|
|
@blueprint.route("/<proposal_id>", methods=["PUT"])
|
|
|
|
|
@requires_team_member_auth
|
2019-03-01 12:11:03 -08:00
|
|
|
|
@body({
|
2019-03-18 11:35:08 -07:00
|
|
|
|
# Length checks are to prevent database errors, not actual user limits imposed
|
2019-03-01 12:11:03 -08:00
|
|
|
|
"title": fields.Str(required=True),
|
|
|
|
|
"brief": fields.Str(required=True),
|
|
|
|
|
"content": fields.Str(required=True),
|
|
|
|
|
"target": fields.Str(required=True),
|
|
|
|
|
"payoutAddress": fields.Str(required=True),
|
|
|
|
|
"milestones": fields.List(fields.Dict(), required=True),
|
2019-03-18 11:35:08 -07:00
|
|
|
|
"rfpOptIn": fields.Bool(required=False, missing=None),
|
2019-03-01 12:11:03 -08:00
|
|
|
|
})
|
2019-03-06 12:25:58 -08:00
|
|
|
|
def update_proposal(milestones, proposal_id, rfp_opt_in, **kwargs):
|
2018-11-13 08:07:09 -08:00
|
|
|
|
# Update the base proposal fields
|
|
|
|
|
try:
|
2019-03-14 13:29:02 -07:00
|
|
|
|
if g.current_proposal.status not in [ProposalStatus.DRAFT,
|
|
|
|
|
ProposalStatus.REJECTED]:
|
|
|
|
|
raise ValidationException(
|
|
|
|
|
f"Proposal with status: {g.current_proposal.status} are not authorized for updates"
|
|
|
|
|
)
|
2018-11-14 08:43:00 -08:00
|
|
|
|
g.current_proposal.update(**kwargs)
|
2018-11-13 08:07:09 -08:00
|
|
|
|
except ValidationException as e:
|
2019-01-27 18:03:38 -08:00
|
|
|
|
return {"message": "{}".format(str(e))}, 400
|
2018-11-13 08:07:09 -08:00
|
|
|
|
db.session.add(g.current_proposal)
|
2019-03-06 12:25:58 -08:00
|
|
|
|
|
|
|
|
|
# twiddle rfp opt-in (modifies proposal matching and/or bounty)
|
|
|
|
|
if rfp_opt_in is not None:
|
|
|
|
|
g.current_proposal.update_rfp_opt_in(rfp_opt_in)
|
|
|
|
|
|
2019-03-14 13:29:02 -07:00
|
|
|
|
Milestone.make(milestones, g.current_proposal)
|
2018-12-14 11:36:22 -08:00
|
|
|
|
|
2018-11-13 08:07:09 -08:00
|
|
|
|
# Commit
|
|
|
|
|
db.session.commit()
|
|
|
|
|
return proposal_schema.dump(g.current_proposal), 200
|
2018-09-10 09:55:26 -07:00
|
|
|
|
|
2018-09-26 01:46:30 -07:00
|
|
|
|
|
2019-02-19 11:48:51 -08:00
|
|
|
|
@blueprint.route("/<proposal_id>/rfp", methods=["DELETE"])
|
|
|
|
|
@requires_team_member_auth
|
|
|
|
|
def unlink_proposal_from_rfp(proposal_id):
|
|
|
|
|
g.current_proposal.rfp_id = None
|
2019-03-06 12:25:58 -08:00
|
|
|
|
# this will zero matching and bounty
|
|
|
|
|
g.current_proposal.update_rfp_opt_in(False)
|
|
|
|
|
g.current_proposal.rfp_opt_in = None
|
2019-02-19 11:48:51 -08:00
|
|
|
|
db.session.add(g.current_proposal)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
return proposal_schema.dump(g.current_proposal), 200
|
|
|
|
|
|
|
|
|
|
|
2018-11-14 08:43:00 -08:00
|
|
|
|
@blueprint.route("/<proposal_id>", methods=["DELETE"])
|
2018-11-13 08:07:09 -08:00
|
|
|
|
@requires_team_member_auth
|
2019-01-09 10:23:08 -08:00
|
|
|
|
def delete_proposal(proposal_id):
|
2019-01-30 09:59:15 -08:00
|
|
|
|
deleteable_statuses = [
|
|
|
|
|
ProposalStatus.DRAFT,
|
|
|
|
|
ProposalStatus.PENDING,
|
|
|
|
|
ProposalStatus.APPROVED,
|
|
|
|
|
ProposalStatus.REJECTED,
|
2019-02-05 17:45:57 -08:00
|
|
|
|
ProposalStatus.STAKING,
|
2019-01-30 09:59:15 -08:00
|
|
|
|
]
|
2019-01-09 10:23:08 -08:00
|
|
|
|
status = g.current_proposal.status
|
|
|
|
|
if status not in deleteable_statuses:
|
|
|
|
|
return {"message": "Cannot delete proposals with %s status" % status}, 400
|
2018-11-13 08:07:09 -08:00
|
|
|
|
db.session.delete(g.current_proposal)
|
|
|
|
|
db.session.commit()
|
2019-03-01 12:11:03 -08:00
|
|
|
|
return {"message": "ok"}, 202
|
2018-09-10 09:55:26 -07:00
|
|
|
|
|
2018-09-26 01:46:30 -07:00
|
|
|
|
|
2019-01-09 10:23:08 -08:00
|
|
|
|
@blueprint.route("/<proposal_id>/submit_for_approval", methods=["PUT"])
|
|
|
|
|
@requires_team_member_auth
|
|
|
|
|
def submit_for_approval_proposal(proposal_id):
|
|
|
|
|
try:
|
2019-01-28 14:46:04 -08:00
|
|
|
|
g.current_proposal.submit_for_approval()
|
2019-01-09 10:23:08 -08:00
|
|
|
|
except ValidationException as e:
|
2019-01-27 18:03:38 -08:00
|
|
|
|
return {"message": "{}".format(str(e))}, 400
|
2019-01-09 10:23:08 -08:00
|
|
|
|
db.session.add(g.current_proposal)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
return proposal_schema.dump(g.current_proposal), 200
|
|
|
|
|
|
|
|
|
|
|
2019-01-31 14:56:16 -08:00
|
|
|
|
@blueprint.route("/<proposal_id>/stake", methods=["GET"])
|
|
|
|
|
@requires_team_member_auth
|
|
|
|
|
def get_proposal_stake(proposal_id):
|
|
|
|
|
if g.current_proposal.status != ProposalStatus.STAKING:
|
2019-03-01 12:11:03 -08:00
|
|
|
|
return {"message": "ok"}, 400
|
2019-01-31 14:56:16 -08:00
|
|
|
|
contribution = g.current_proposal.get_staking_contribution(g.current_user.id)
|
|
|
|
|
if contribution:
|
|
|
|
|
return proposal_contribution_schema.dump(contribution)
|
2019-03-01 12:11:03 -08:00
|
|
|
|
return {"message": "ok"}, 404
|
2019-01-31 14:56:16 -08:00
|
|
|
|
|
|
|
|
|
|
2018-11-13 08:07:09 -08:00
|
|
|
|
@blueprint.route("/<proposal_id>/publish", methods=["PUT"])
|
|
|
|
|
@requires_team_member_auth
|
2018-12-28 15:05:34 -08:00
|
|
|
|
def publish_proposal(proposal_id):
|
2018-09-10 09:55:26 -07:00
|
|
|
|
try:
|
2018-11-13 08:07:09 -08:00
|
|
|
|
g.current_proposal.publish()
|
|
|
|
|
except ValidationException as e:
|
2019-01-27 18:03:38 -08:00
|
|
|
|
return {"message": "{}".format(str(e))}, 400
|
2018-11-13 08:07:09 -08:00
|
|
|
|
db.session.add(g.current_proposal)
|
2019-02-19 09:13:13 -08:00
|
|
|
|
|
|
|
|
|
task = ProposalDeadline(g.current_proposal)
|
|
|
|
|
task.make_task()
|
|
|
|
|
|
2018-11-13 08:07:09 -08:00
|
|
|
|
db.session.commit()
|
|
|
|
|
return proposal_schema.dump(g.current_proposal), 200
|
2018-11-02 09:24:28 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/<proposal_id>/updates", methods=["GET"])
|
|
|
|
|
def get_proposal_updates(proposal_id):
|
2018-11-07 09:33:19 -08:00
|
|
|
|
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
2018-11-02 09:24:28 -07:00
|
|
|
|
if proposal:
|
|
|
|
|
dumped_proposal = proposal_schema.dump(proposal)
|
|
|
|
|
return dumped_proposal["updates"]
|
|
|
|
|
else:
|
|
|
|
|
return {"message": "No proposal matching id"}, 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/<proposal_id>/updates/<update_id>", methods=["GET"])
|
|
|
|
|
def get_proposal_update(proposal_id, update_id):
|
2018-11-07 09:33:19 -08:00
|
|
|
|
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
2018-11-02 09:24:28 -07:00
|
|
|
|
if proposal:
|
|
|
|
|
update = ProposalUpdate.query.filter_by(proposal_id=proposal.id, id=update_id).first()
|
|
|
|
|
if update:
|
2019-01-04 11:03:37 -08:00
|
|
|
|
return proposal_update_schema.dump(update)
|
2018-11-02 09:24:28 -07:00
|
|
|
|
else:
|
|
|
|
|
return {"message": "No update matching id"}
|
|
|
|
|
else:
|
|
|
|
|
return {"message": "No proposal matching id"}, 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/<proposal_id>/updates", methods=["POST"])
|
2019-03-12 20:35:38 -07:00
|
|
|
|
@limiter.limit("5/day;1/minute")
|
2018-11-07 11:19:12 -08:00
|
|
|
|
@requires_team_member_auth
|
2019-03-01 12:11:03 -08:00
|
|
|
|
@body({
|
2019-03-18 12:03:01 -07:00
|
|
|
|
"title": fields.Str(required=True, validate=validate.Length(min=3, max=60)),
|
|
|
|
|
"content": fields.Str(required=True, validate=validate.Length(min=5, max=10000)),
|
2019-03-01 12:11:03 -08:00
|
|
|
|
})
|
2018-11-02 09:24:28 -07:00
|
|
|
|
def post_proposal_update(proposal_id, title, content):
|
2018-11-07 11:19:12 -08:00
|
|
|
|
update = ProposalUpdate(
|
|
|
|
|
proposal_id=g.current_proposal.id,
|
|
|
|
|
title=title,
|
|
|
|
|
content=content
|
|
|
|
|
)
|
|
|
|
|
db.session.add(update)
|
|
|
|
|
db.session.commit()
|
2018-11-02 09:24:28 -07:00
|
|
|
|
|
2019-03-14 09:46:09 -07:00
|
|
|
|
# Send email to all contributors
|
|
|
|
|
for u in g.current_proposal.contributors:
|
|
|
|
|
send_email(u.email_address, 'contribution_update', {
|
|
|
|
|
'proposal': g.current_proposal,
|
|
|
|
|
'proposal_update': update,
|
|
|
|
|
'update_url': make_url(f'/proposals/{proposal_id}?tab=updates&update={update.id}'),
|
|
|
|
|
})
|
2019-01-16 14:26:45 -08:00
|
|
|
|
|
2019-10-23 14:34:10 -07:00
|
|
|
|
# Send email to all followers
|
|
|
|
|
g.current_proposal.send_follower_email(
|
|
|
|
|
"followed_proposal_update", url_suffix="?tab=updates"
|
|
|
|
|
)
|
|
|
|
|
|
2018-11-07 11:19:12 -08:00
|
|
|
|
dumped_update = proposal_update_schema.dump(update)
|
|
|
|
|
return dumped_update, 201
|
2018-11-21 19:18:22 -08:00
|
|
|
|
|
2018-12-14 11:36:22 -08:00
|
|
|
|
|
2018-11-16 08:16:52 -08:00
|
|
|
|
@blueprint.route("/<proposal_id>/invite", methods=["POST"])
|
2019-03-12 20:35:38 -07:00
|
|
|
|
@limiter.limit("30/day;10/minute")
|
2018-11-16 08:16:52 -08:00
|
|
|
|
@requires_team_member_auth
|
2019-03-01 12:11:03 -08:00
|
|
|
|
@body({
|
2019-03-18 12:03:01 -07:00
|
|
|
|
"address": fields.Str(required=True, validate=validate.Length(max=255)),
|
2019-03-01 12:11:03 -08:00
|
|
|
|
})
|
2018-11-16 08:16:52 -08:00
|
|
|
|
def post_proposal_team_invite(proposal_id, address):
|
2019-03-18 09:51:46 -07:00
|
|
|
|
for u in g.current_proposal.team:
|
|
|
|
|
if address == u.email_address:
|
|
|
|
|
return {"message": f"Cannot invite members already on the team"}, 400
|
|
|
|
|
|
2019-03-12 20:35:38 -07:00
|
|
|
|
existing_invite = ProposalTeamInvite.query.filter_by(
|
|
|
|
|
proposal_id=proposal_id,
|
|
|
|
|
address=address
|
|
|
|
|
).first()
|
|
|
|
|
if existing_invite:
|
|
|
|
|
return {"message": f"You've already invited {address}"}, 400
|
|
|
|
|
|
2018-11-16 08:16:52 -08:00
|
|
|
|
invite = ProposalTeamInvite(
|
|
|
|
|
proposal_id=proposal_id,
|
|
|
|
|
address=address
|
|
|
|
|
)
|
|
|
|
|
db.session.add(invite)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
# Send email
|
|
|
|
|
email = address
|
2018-12-14 11:36:22 -08:00
|
|
|
|
user = User.get_by_email(email_address=address)
|
2018-11-16 08:16:52 -08:00
|
|
|
|
if user:
|
|
|
|
|
email = user.email_address
|
|
|
|
|
if is_email(email):
|
|
|
|
|
send_email(email, 'team_invite', {
|
|
|
|
|
'user': user,
|
|
|
|
|
'inviter': g.current_user,
|
2018-12-03 18:45:18 -08:00
|
|
|
|
'proposal': g.current_proposal,
|
2019-01-15 12:19:59 -08:00
|
|
|
|
'invite_url': make_url(
|
|
|
|
|
f'/profile/{user.id}?tab=invites' if user else '/auth')
|
2018-11-16 08:16:52 -08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return proposal_team_invite_schema.dump(invite), 201
|
|
|
|
|
|
2018-11-26 17:14:00 -08:00
|
|
|
|
|
2018-11-16 08:16:52 -08:00
|
|
|
|
@blueprint.route("/<proposal_id>/invite/<id_or_address>", methods=["DELETE"])
|
|
|
|
|
@requires_team_member_auth
|
|
|
|
|
def delete_proposal_team_invite(proposal_id, id_or_address):
|
|
|
|
|
invite = ProposalTeamInvite.query.filter(
|
|
|
|
|
(ProposalTeamInvite.id == id_or_address) |
|
|
|
|
|
(ProposalTeamInvite.address == id_or_address)
|
|
|
|
|
).first()
|
|
|
|
|
if not invite:
|
|
|
|
|
return {"message": "No invite found given {}".format(id_or_address)}, 404
|
|
|
|
|
if invite.accepted:
|
|
|
|
|
return {"message": "Cannot delete an invite that has been accepted"}, 403
|
|
|
|
|
|
|
|
|
|
db.session.delete(invite)
|
|
|
|
|
db.session.commit()
|
2019-03-01 12:11:03 -08:00
|
|
|
|
return {"message": "ok"}, 202
|
2018-11-26 17:14:00 -08:00
|
|
|
|
|
2018-11-21 19:18:22 -08:00
|
|
|
|
|
|
|
|
|
@blueprint.route("/<proposal_id>/contributions", methods=["GET"])
|
|
|
|
|
def get_proposal_contributions(proposal_id):
|
|
|
|
|
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
2019-01-09 12:48:41 -08:00
|
|
|
|
if not proposal:
|
2018-11-21 19:18:22 -08:00
|
|
|
|
return {"message": "No proposal matching id"}, 404
|
2019-01-22 21:35:22 -08:00
|
|
|
|
|
2019-03-14 13:29:02 -07:00
|
|
|
|
top_contributions = ProposalContribution.query.filter_by(
|
2019-03-01 12:11:03 -08:00
|
|
|
|
proposal_id=proposal_id,
|
|
|
|
|
status=ContributionStatus.CONFIRMED,
|
|
|
|
|
staking=False,
|
2019-03-14 13:29:02 -07:00
|
|
|
|
).order_by(
|
|
|
|
|
ProposalContribution.amount.desc()
|
|
|
|
|
).limit(
|
|
|
|
|
5
|
|
|
|
|
).all()
|
|
|
|
|
latest_contributions = ProposalContribution.query.filter_by(
|
2019-03-01 12:11:03 -08:00
|
|
|
|
proposal_id=proposal_id,
|
|
|
|
|
status=ContributionStatus.CONFIRMED,
|
|
|
|
|
staking=False,
|
2019-03-14 13:29:02 -07:00
|
|
|
|
).order_by(
|
|
|
|
|
ProposalContribution.date_created.desc()
|
|
|
|
|
).limit(
|
|
|
|
|
5
|
|
|
|
|
).all()
|
2019-01-22 21:35:22 -08:00
|
|
|
|
|
2019-01-09 12:48:41 -08:00
|
|
|
|
return {
|
|
|
|
|
'top': proposal_proposal_contributions_schema.dump(top_contributions),
|
|
|
|
|
'latest': proposal_proposal_contributions_schema.dump(latest_contributions),
|
|
|
|
|
}
|
2018-11-21 19:18:22 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/<proposal_id>/contributions/<contribution_id>", methods=["GET"])
|
|
|
|
|
def get_proposal_contribution(proposal_id, contribution_id):
|
|
|
|
|
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
2019-03-12 14:35:15 -07:00
|
|
|
|
if not proposal:
|
2018-11-21 19:18:22 -08:00
|
|
|
|
return {"message": "No proposal matching id"}, 404
|
|
|
|
|
|
2019-03-12 14:35:15 -07:00
|
|
|
|
contribution = ProposalContribution.query.filter_by(id=contribution_id).first()
|
|
|
|
|
if not contribution:
|
|
|
|
|
return {"message": "No contribution matching id"}, 404
|
|
|
|
|
|
|
|
|
|
return proposal_contribution_schema.dump(contribution)
|
|
|
|
|
|
2018-11-21 19:18:22 -08:00
|
|
|
|
|
|
|
|
|
@blueprint.route("/<proposal_id>/contributions", methods=["POST"])
|
2019-03-12 20:35:38 -07:00
|
|
|
|
@limiter.limit("30/day;10/hour;2/minute")
|
2019-03-01 12:11:03 -08:00
|
|
|
|
@body({
|
2019-03-14 13:29:02 -07:00
|
|
|
|
"amount": fields.Str(required=True, validate=lambda p: 0.0001 <= float(p) <= 1000000),
|
2019-06-11 19:49:14 -07:00
|
|
|
|
"private": fields.Bool(required=False, missing=True)
|
2019-03-01 12:11:03 -08:00
|
|
|
|
})
|
2019-06-11 19:49:14 -07:00
|
|
|
|
def post_proposal_contribution(proposal_id, amount, private):
|
2018-11-21 19:18:22 -08:00
|
|
|
|
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
2019-01-06 14:48:07 -08:00
|
|
|
|
if not proposal:
|
|
|
|
|
return {"message": "No proposal matching id"}, 404
|
|
|
|
|
|
2019-01-06 22:58:33 -08:00
|
|
|
|
code = 200
|
2019-06-11 19:49:14 -07:00
|
|
|
|
user = get_authed_user()
|
2019-02-23 12:31:07 -08:00
|
|
|
|
contribution = None
|
|
|
|
|
|
|
|
|
|
if user:
|
2019-03-13 16:36:24 -07:00
|
|
|
|
contribution = ProposalContribution \
|
2019-06-11 19:49:14 -07:00
|
|
|
|
.get_existing_contribution(user.id, proposal_id, amount, private)
|
2019-01-06 22:58:33 -08:00
|
|
|
|
|
|
|
|
|
if not contribution:
|
|
|
|
|
code = 201
|
2019-02-23 12:31:07 -08:00
|
|
|
|
contribution = proposal.create_contribution(
|
|
|
|
|
amount=amount,
|
2019-06-11 19:49:14 -07:00
|
|
|
|
private=private,
|
2019-02-23 12:31:07 -08:00
|
|
|
|
user_id=user.id if user else None,
|
|
|
|
|
)
|
2019-01-06 22:58:33 -08:00
|
|
|
|
|
2019-01-06 14:48:07 -08:00
|
|
|
|
dumped_contribution = proposal_contribution_schema.dump(contribution)
|
2019-01-06 22:58:33 -08:00
|
|
|
|
return dumped_contribution, code
|
2019-01-08 14:58:32 -08:00
|
|
|
|
|
2019-01-09 13:32:51 -08:00
|
|
|
|
|
|
|
|
|
# Can't use <proposal_id> since webhook doesn't know proposal id
|
2019-01-08 14:58:32 -08:00
|
|
|
|
@blueprint.route("/contribution/<contribution_id>/confirm", methods=["POST"])
|
|
|
|
|
@internal_webhook
|
2019-03-01 12:11:03 -08:00
|
|
|
|
@body({
|
|
|
|
|
"to": fields.Str(required=True),
|
|
|
|
|
"amount": fields.Str(required=True),
|
|
|
|
|
"txid": fields.Str(required=True),
|
|
|
|
|
})
|
2019-01-08 14:58:32 -08:00
|
|
|
|
def post_contribution_confirmation(contribution_id, to, amount, txid):
|
2019-01-16 14:26:45 -08:00
|
|
|
|
contribution = ProposalContribution.query.filter_by(
|
2019-01-08 14:58:32 -08:00
|
|
|
|
id=contribution_id).first()
|
|
|
|
|
|
|
|
|
|
if not contribution:
|
2019-03-13 14:39:50 -07:00
|
|
|
|
msg = f'Unknown contribution {contribution_id} confirmed with txid {txid}, amount {amount}'
|
|
|
|
|
capture_message(msg)
|
|
|
|
|
current_app.logger.warn(msg)
|
2019-01-08 14:58:32 -08:00
|
|
|
|
return {"message": "No contribution matching id"}, 404
|
|
|
|
|
|
2019-01-30 09:59:15 -08:00
|
|
|
|
if contribution.status == ContributionStatus.CONFIRMED:
|
2019-01-16 14:26:45 -08:00
|
|
|
|
# Duplicates can happen, just return ok
|
2019-03-01 12:11:03 -08:00
|
|
|
|
return {"message": "ok"}, 200
|
2019-01-16 14:26:45 -08:00
|
|
|
|
|
2019-01-09 11:35:37 -08:00
|
|
|
|
# Convert to whole zcash coins from zats
|
|
|
|
|
zec_amount = str(from_zat(int(amount)))
|
|
|
|
|
|
|
|
|
|
contribution.confirm(tx_id=txid, amount=zec_amount)
|
2019-01-08 14:58:32 -08:00
|
|
|
|
db.session.add(contribution)
|
2019-02-13 08:54:46 -08:00
|
|
|
|
db.session.flush()
|
2019-01-16 14:26:45 -08:00
|
|
|
|
|
2019-01-31 14:56:16 -08:00
|
|
|
|
if contribution.proposal.status == ProposalStatus.STAKING:
|
2019-02-15 19:35:25 -08:00
|
|
|
|
contribution.proposal.set_pending_when_ready()
|
2019-01-16 14:26:45 -08:00
|
|
|
|
|
2019-01-31 14:56:16 -08:00
|
|
|
|
# email progress of staking, partial or complete
|
|
|
|
|
send_email(contribution.user.email_address, 'staking_contribution_confirmed', {
|
|
|
|
|
'contribution': contribution,
|
2019-01-16 14:26:45 -08:00
|
|
|
|
'proposal': contribution.proposal,
|
2019-03-14 21:16:38 -07:00
|
|
|
|
'tx_explorer_url': make_explore_url(txid),
|
2019-01-31 14:56:16 -08:00
|
|
|
|
'fully_staked': contribution.proposal.is_staked,
|
2019-02-05 17:45:57 -08:00
|
|
|
|
'stake_target': str(PROPOSAL_STAKING_AMOUNT.normalize()),
|
2019-01-31 14:56:16 -08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
# Send to the user
|
2019-02-25 11:26:43 -08:00
|
|
|
|
if contribution.user:
|
|
|
|
|
send_email(contribution.user.email_address, 'contribution_confirmed', {
|
|
|
|
|
'contribution': contribution,
|
|
|
|
|
'proposal': contribution.proposal,
|
2019-03-14 21:16:38 -07:00
|
|
|
|
'tx_explorer_url': make_explore_url(txid),
|
2019-02-25 11:26:43 -08:00
|
|
|
|
})
|
2019-01-16 14:26:45 -08:00
|
|
|
|
|
2019-01-31 14:56:16 -08:00
|
|
|
|
# Send to the full proposal gang
|
|
|
|
|
for member in contribution.proposal.team:
|
|
|
|
|
send_email(member.email_address, 'proposal_contribution', {
|
|
|
|
|
'proposal': contribution.proposal,
|
|
|
|
|
'contribution': contribution,
|
|
|
|
|
'contributor': contribution.user,
|
|
|
|
|
'funded': contribution.proposal.funded,
|
|
|
|
|
'proposal_url': make_url(f'/proposals/{contribution.proposal.id}'),
|
2019-02-25 11:26:43 -08:00
|
|
|
|
'contributor_url': make_url(f'/profile/{contribution.user.id}') if contribution.user else '',
|
2019-01-31 14:56:16 -08:00
|
|
|
|
})
|
|
|
|
|
|
2019-02-13 08:54:46 -08:00
|
|
|
|
db.session.commit()
|
2019-03-01 12:11:03 -08:00
|
|
|
|
return {"message": "ok"}, 200
|
2019-01-09 13:32:51 -08:00
|
|
|
|
|
2019-01-22 21:35:22 -08:00
|
|
|
|
|
2019-01-09 13:32:51 -08:00
|
|
|
|
@blueprint.route("/contribution/<contribution_id>", methods=["DELETE"])
|
|
|
|
|
@requires_auth
|
|
|
|
|
def delete_proposal_contribution(contribution_id):
|
2019-03-01 12:11:03 -08:00
|
|
|
|
contribution = ProposalContribution.query.filter_by(
|
2019-01-09 13:32:51 -08:00
|
|
|
|
id=contribution_id).first()
|
|
|
|
|
if not contribution:
|
|
|
|
|
return {"message": "No contribution matching id"}, 404
|
|
|
|
|
|
2019-01-30 09:59:15 -08:00
|
|
|
|
if contribution.status == ContributionStatus.CONFIRMED:
|
2019-01-09 13:32:51 -08:00
|
|
|
|
return {"message": "Cannot delete confirmed contributions"}, 400
|
|
|
|
|
|
|
|
|
|
if contribution.user_id != g.current_user.id:
|
|
|
|
|
return {"message": "Must be the user of the contribution to delete it"}, 403
|
|
|
|
|
|
2019-01-30 09:59:15 -08:00
|
|
|
|
contribution.status = ContributionStatus.DELETED
|
2019-01-09 13:32:51 -08:00
|
|
|
|
db.session.add(contribution)
|
|
|
|
|
db.session.commit()
|
2019-03-01 12:11:03 -08:00
|
|
|
|
return {"message": "ok"}, 202
|
2019-02-11 21:10:09 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# request MS payout
|
|
|
|
|
@blueprint.route("/<proposal_id>/milestone/<milestone_id>/request", methods=["PUT"])
|
|
|
|
|
@requires_team_member_auth
|
|
|
|
|
def request_milestone_payout(proposal_id, milestone_id):
|
2019-02-13 08:54:46 -08:00
|
|
|
|
if not g.current_proposal.is_funded:
|
|
|
|
|
return {"message": "Proposal is not fully funded"}, 400
|
2019-02-11 21:10:09 -08:00
|
|
|
|
for ms in g.current_proposal.milestones:
|
2019-02-13 08:54:46 -08:00
|
|
|
|
if ms.id == int(milestone_id):
|
2019-02-11 21:10:09 -08:00
|
|
|
|
ms.request_payout(g.current_user.id)
|
|
|
|
|
db.session.add(ms)
|
|
|
|
|
db.session.commit()
|
2019-02-13 12:30:58 -08:00
|
|
|
|
# email ARBITER to review payout request
|
|
|
|
|
send_email(g.current_proposal.arbiter.user.email_address, 'milestone_request', {
|
|
|
|
|
'proposal': g.current_proposal,
|
|
|
|
|
'proposal_milestones_url': make_url(f'/proposals/{g.current_proposal.id}?tab=milestones'),
|
|
|
|
|
})
|
2019-02-11 21:10:09 -08:00
|
|
|
|
return proposal_schema.dump(g.current_proposal), 200
|
2019-02-13 08:54:46 -08:00
|
|
|
|
|
|
|
|
|
return {"message": "No milestone matching id"}, 404
|
2019-02-11 21:10:09 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# accept MS payout (arbiter)
|
|
|
|
|
@blueprint.route("/<proposal_id>/milestone/<milestone_id>/accept", methods=["PUT"])
|
|
|
|
|
@requires_arbiter_auth
|
|
|
|
|
def accept_milestone_payout_request(proposal_id, milestone_id):
|
2019-02-13 08:54:46 -08:00
|
|
|
|
if not g.current_proposal.is_funded:
|
|
|
|
|
return {"message": "Proposal is not fully funded"}, 400
|
2019-02-11 21:10:09 -08:00
|
|
|
|
for ms in g.current_proposal.milestones:
|
2019-02-13 08:54:46 -08:00
|
|
|
|
if ms.id == int(milestone_id):
|
2019-02-11 21:10:09 -08:00
|
|
|
|
ms.accept_request(g.current_user.id)
|
|
|
|
|
db.session.add(ms)
|
|
|
|
|
db.session.commit()
|
2019-02-13 12:30:58 -08:00
|
|
|
|
# email TEAM that payout request accepted
|
|
|
|
|
amount = Decimal(ms.payout_percent) * Decimal(g.current_proposal.target) / 100
|
|
|
|
|
for member in g.current_proposal.team:
|
|
|
|
|
send_email(member.email_address, 'milestone_accept', {
|
|
|
|
|
'proposal': g.current_proposal,
|
|
|
|
|
'amount': amount,
|
|
|
|
|
'proposal_milestones_url': make_url(f'/proposals/{g.current_proposal.id}?tab=milestones'),
|
|
|
|
|
})
|
2019-02-11 21:10:09 -08:00
|
|
|
|
return proposal_schema.dump(g.current_proposal), 200
|
2019-02-13 08:54:46 -08:00
|
|
|
|
|
|
|
|
|
return {"message": "No milestone matching id"}, 404
|
2019-02-11 21:10:09 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# reject MS payout (arbiter) (reason)
|
|
|
|
|
@blueprint.route("/<proposal_id>/milestone/<milestone_id>/reject", methods=["PUT"])
|
|
|
|
|
@requires_arbiter_auth
|
2019-03-01 12:11:03 -08:00
|
|
|
|
@body({
|
2019-03-18 12:03:01 -07:00
|
|
|
|
"reason": fields.Str(required=True, validate=validate.Length(min=2, max=200)),
|
2019-03-01 12:11:03 -08:00
|
|
|
|
})
|
2019-02-11 21:10:09 -08:00
|
|
|
|
def reject_milestone_payout_request(proposal_id, milestone_id, reason):
|
2019-02-13 08:54:46 -08:00
|
|
|
|
if not g.current_proposal.is_funded:
|
|
|
|
|
return {"message": "Proposal is not fully funded"}, 400
|
2019-02-11 21:10:09 -08:00
|
|
|
|
for ms in g.current_proposal.milestones:
|
2019-02-13 08:54:46 -08:00
|
|
|
|
if ms.id == int(milestone_id):
|
2019-02-11 21:10:09 -08:00
|
|
|
|
ms.reject_request(g.current_user.id, reason)
|
|
|
|
|
db.session.add(ms)
|
|
|
|
|
db.session.commit()
|
2019-02-13 12:30:58 -08:00
|
|
|
|
# email TEAM that payout request was rejected
|
|
|
|
|
for member in g.current_proposal.team:
|
|
|
|
|
send_email(member.email_address, 'milestone_reject', {
|
|
|
|
|
'proposal': g.current_proposal,
|
|
|
|
|
'admin_note': reason,
|
|
|
|
|
'proposal_milestones_url': make_url(f'/proposals/{g.current_proposal.id}?tab=milestones'),
|
|
|
|
|
})
|
2019-02-11 21:10:09 -08:00
|
|
|
|
return proposal_schema.dump(g.current_proposal), 200
|
|
|
|
|
|
2019-02-13 08:54:46 -08:00
|
|
|
|
return {"message": "No milestone matching id"}, 404
|
2019-10-23 14:34:10 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/<proposal_id>/follow", methods=["PUT"])
|
|
|
|
|
@requires_auth
|
|
|
|
|
@body({"isFollow": fields.Bool(required=True)})
|
|
|
|
|
def follow_proposal(proposal_id, is_follow):
|
|
|
|
|
user = g.current_user
|
|
|
|
|
# Make sure proposal exists
|
|
|
|
|
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
|
|
|
|
if not proposal:
|
|
|
|
|
return {"message": "No proposal matching id"}, 404
|
|
|
|
|
|
|
|
|
|
proposal.follow(user, is_follow)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
return {"message": "ok"}, 200
|
|
|
|
|
|
2019-10-24 10:32:00 -07:00
|
|
|
|
|
|
|
|
|
@blueprint.route("/<proposal_id>/like", methods=["PUT"])
|
|
|
|
|
@requires_auth
|
|
|
|
|
@body({"isLiked": fields.Bool(required=True)})
|
|
|
|
|
def like_proposal(proposal_id, is_liked):
|
|
|
|
|
user = g.current_user
|
|
|
|
|
# Make sure proposal exists
|
|
|
|
|
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
|
|
|
|
if not proposal:
|
|
|
|
|
return {"message": "No proposal matching id"}, 404
|
|
|
|
|
|
|
|
|
|
if not proposal.status == ProposalStatus.LIVE:
|
|
|
|
|
return {"message": "Cannot like a proposal that's not live"}, 404
|
|
|
|
|
|
|
|
|
|
proposal.like(user, is_liked)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
return {"message": "ok"}, 200
|
|
|
|
|
|