2018-11-14 09:59:48 -08:00
|
|
|
|
from dateutil.parser import parse
|
2018-11-07 11:19:12 -08:00
|
|
|
|
from flask import Blueprint, g
|
2018-10-22 15:31:33 -07:00
|
|
|
|
from flask_yoloapi import endpoint, parameter
|
2018-11-08 10:42:19 -08:00
|
|
|
|
from grant.comment.models import Comment, comment_schema, comments_schema
|
2018-11-16 08:16:52 -08:00
|
|
|
|
from grant.email.send import send_email
|
2019-01-23 07:00:30 -08:00
|
|
|
|
from grant.milestone.models import Milestone
|
|
|
|
|
from grant.user.models import User
|
2019-01-09 13:57:15 -08:00
|
|
|
|
from grant.utils.auth import requires_auth, requires_team_member_auth, get_authed_user, internal_webhook
|
2018-11-13 08:07:09 -08:00
|
|
|
|
from grant.utils.exceptions import ValidationException
|
2019-01-16 14:26:45 -08:00
|
|
|
|
from grant.utils.misc import is_email, make_url, from_zat, make_preview
|
2019-01-23 07:00:30 -08:00
|
|
|
|
from sqlalchemy import or_
|
|
|
|
|
|
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,
|
|
|
|
|
DRAFT,
|
|
|
|
|
PENDING,
|
|
|
|
|
APPROVED,
|
|
|
|
|
REJECTED,
|
|
|
|
|
LIVE,
|
2019-01-09 13:32:51 -08:00
|
|
|
|
DELETED,
|
2019-01-09 13:57:15 -08:00
|
|
|
|
CONFIRMED,
|
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"])
|
2018-11-04 10:33:22 -08:00
|
|
|
|
@endpoint.api()
|
2018-09-10 09:55:26 -07:00
|
|
|
|
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-09 10:23:08 -08:00
|
|
|
|
if proposal.status != LIVE:
|
|
|
|
|
if proposal.status == DELETED:
|
|
|
|
|
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"])
|
2018-11-04 10:33:22 -08:00
|
|
|
|
@endpoint.api()
|
2018-09-10 09:55:26 -07:00
|
|
|
|
def get_proposal_comments(proposal_id):
|
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
|
|
|
|
|
2018-11-08 10:42:19 -08:00
|
|
|
|
# Only pull top comments, replies will be attached to them
|
|
|
|
|
comments = Comment.query.filter_by(proposal_id=proposal_id, parent_comment_id=None)
|
|
|
|
|
num_comments = Comment.query.filter_by(proposal_id=proposal_id).count()
|
|
|
|
|
return {
|
|
|
|
|
"proposalId": proposal_id,
|
|
|
|
|
"totalComments": num_comments,
|
|
|
|
|
"comments": comments_schema.dump(comments)
|
|
|
|
|
}
|
2018-09-10 09:55:26 -07:00
|
|
|
|
|
|
|
|
|
|
2018-09-18 15:17:34 -07:00
|
|
|
|
@blueprint.route("/<proposal_id>/comments", methods=["POST"])
|
2018-12-14 11:36:22 -08:00
|
|
|
|
@requires_auth
|
2018-10-22 15:31:33 -07:00
|
|
|
|
@endpoint.api(
|
2018-11-08 10:29:29 -08:00
|
|
|
|
parameter('comment', type=str, required=True),
|
2018-12-14 11:36:22 -08:00
|
|
|
|
parameter('parentCommentId', type=int, required=False)
|
2018-10-22 15:31:33 -07:00
|
|
|
|
)
|
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
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
# TODO: Email proposal team if top-level comment
|
|
|
|
|
preview = make_preview(comment.content, 60)
|
|
|
|
|
if not parent:
|
|
|
|
|
for member in proposal.team:
|
|
|
|
|
send_email(member.email_address, 'proposal_comment', {
|
|
|
|
|
'author': g.current_user,
|
|
|
|
|
'proposal': proposal,
|
|
|
|
|
'preview': preview,
|
|
|
|
|
'comment_url': make_url(f'/proposal/{proposal.id}?tab=discussions&comment={comment.id}'),
|
|
|
|
|
'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,
|
|
|
|
|
'preview': preview,
|
|
|
|
|
'comment_url': make_url(f'/proposal/{proposal.id}?tab=discussions&comment={comment.id}'),
|
|
|
|
|
'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"])
|
2018-10-22 15:31:33 -07:00
|
|
|
|
@endpoint.api(
|
|
|
|
|
parameter('stage', type=str, required=False)
|
|
|
|
|
)
|
|
|
|
|
def get_proposals(stage):
|
2018-09-10 09:55:26 -07:00
|
|
|
|
if stage:
|
|
|
|
|
proposals = (
|
2019-01-09 10:23:08 -08:00
|
|
|
|
Proposal.query.filter_by(status=LIVE, stage=stage)
|
2019-01-23 07:00:30 -08:00
|
|
|
|
.order_by(Proposal.date_created.desc())
|
|
|
|
|
.all()
|
2018-09-10 09:55:26 -07:00
|
|
|
|
)
|
|
|
|
|
else:
|
2018-12-28 15:05:34 -08:00
|
|
|
|
proposals = (
|
2019-01-09 10:23:08 -08:00
|
|
|
|
Proposal.query.filter_by(status=LIVE)
|
2019-01-23 07:00:30 -08:00
|
|
|
|
.order_by(Proposal.date_created.desc())
|
|
|
|
|
.all()
|
2018-12-28 15:05:34 -08:00
|
|
|
|
)
|
2018-09-25 13:09:25 -07:00
|
|
|
|
dumped_proposals = proposals_schema.dump(proposals)
|
2018-12-14 11:36:22 -08:00
|
|
|
|
return dumped_proposals
|
|
|
|
|
# except Exception as e:
|
|
|
|
|
# print(e)
|
|
|
|
|
# print(traceback.format_exc())
|
|
|
|
|
# return {"message": "Oops! Something went wrong."}, 500
|
2018-09-10 09:55:26 -07:00
|
|
|
|
|
|
|
|
|
|
2018-11-13 08:07:09 -08:00
|
|
|
|
@blueprint.route("/drafts", methods=["POST"])
|
2018-12-14 11:36:22 -08:00
|
|
|
|
@requires_auth
|
2018-11-13 08:07:09 -08:00
|
|
|
|
@endpoint.api()
|
|
|
|
|
def make_proposal_draft():
|
|
|
|
|
proposal = Proposal.create(status="DRAFT")
|
|
|
|
|
proposal.team.append(g.current_user)
|
|
|
|
|
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
|
|
|
|
@endpoint.api()
|
|
|
|
|
def get_proposal_drafts():
|
|
|
|
|
proposals = (
|
|
|
|
|
Proposal.query
|
2019-01-23 07:00:30 -08:00
|
|
|
|
.filter(or_(Proposal.status == DRAFT, Proposal.status == REJECTED))
|
|
|
|
|
.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
|
2018-10-22 15:31:33 -07:00
|
|
|
|
@endpoint.api(
|
2018-11-13 08:07:09 -08:00
|
|
|
|
parameter('title', type=str),
|
|
|
|
|
parameter('brief', type=str),
|
|
|
|
|
parameter('category', type=str),
|
2018-11-14 09:27:40 -08:00
|
|
|
|
parameter('content', type=str),
|
2018-11-13 08:07:09 -08:00
|
|
|
|
parameter('target', type=str),
|
|
|
|
|
parameter('payoutAddress', type=str),
|
|
|
|
|
parameter('deadlineDuration', type=int),
|
|
|
|
|
parameter('milestones', type=list)
|
2018-10-22 15:31:33 -07:00
|
|
|
|
)
|
2018-11-14 08:43:00 -08:00
|
|
|
|
def update_proposal(milestones, proposal_id, **kwargs):
|
2018-11-13 08:07:09 -08:00
|
|
|
|
# Update the base proposal fields
|
|
|
|
|
try:
|
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:
|
|
|
|
|
return {"message": "Invalid proposal parameters: {}".format(str(e))}, 400
|
|
|
|
|
db.session.add(g.current_proposal)
|
2018-09-26 01:46:30 -07:00
|
|
|
|
|
2018-11-13 08:07:09 -08:00
|
|
|
|
# Delete & re-add milestones
|
2018-11-14 09:59:48 -08:00
|
|
|
|
[db.session.delete(x) for x in g.current_proposal.milestones]
|
2018-11-13 08:07:09 -08:00
|
|
|
|
if milestones:
|
|
|
|
|
for mdata in milestones:
|
|
|
|
|
m = Milestone(
|
|
|
|
|
title=mdata["title"],
|
2018-11-14 09:59:48 -08:00
|
|
|
|
content=mdata["content"],
|
|
|
|
|
date_estimated=parse(mdata["dateEstimated"]),
|
2018-11-13 08:07:09 -08:00
|
|
|
|
payout_percent=str(mdata["payoutPercent"]),
|
|
|
|
|
immediate_payout=mdata["immediatePayout"],
|
|
|
|
|
proposal_id=g.current_proposal.id
|
2018-09-25 13:09:25 -07:00
|
|
|
|
)
|
2018-11-13 08:07:09 -08:00
|
|
|
|
db.session.add(m)
|
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
|
|
|
|
|
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
|
|
|
|
|
@endpoint.api()
|
2019-01-09 10:23:08 -08:00
|
|
|
|
def delete_proposal(proposal_id):
|
|
|
|
|
deleteable_statuses = [DRAFT, PENDING, APPROVED, REJECTED]
|
|
|
|
|
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()
|
|
|
|
|
return None, 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
|
|
|
|
|
@endpoint.api()
|
|
|
|
|
def submit_for_approval_proposal(proposal_id):
|
|
|
|
|
try:
|
|
|
|
|
g.current_proposal.submit_for_approval()
|
|
|
|
|
except ValidationException as e:
|
|
|
|
|
return {"message": "Invalid proposal parameters: {}".format(str(e))}, 400
|
|
|
|
|
db.session.add(g.current_proposal)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
return proposal_schema.dump(g.current_proposal), 200
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
@endpoint.api()
|
|
|
|
|
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:
|
|
|
|
|
return {"message": "Invalid proposal parameters: {}".format(str(e))}, 400
|
|
|
|
|
db.session.add(g.current_proposal)
|
|
|
|
|
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"])
|
|
|
|
|
@endpoint.api()
|
|
|
|
|
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"])
|
|
|
|
|
@endpoint.api()
|
|
|
|
|
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"])
|
2018-11-07 11:19:12 -08:00
|
|
|
|
@requires_team_member_auth
|
2018-11-02 09:24:28 -07:00
|
|
|
|
@endpoint.api(
|
|
|
|
|
parameter('title', type=str, required=True),
|
|
|
|
|
parameter('content', type=str, required=True)
|
|
|
|
|
)
|
|
|
|
|
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-01-16 14:26:45 -08:00
|
|
|
|
# Send email to all contributors (even if contribution failed)
|
|
|
|
|
preview = make_preview(update.content, 200)
|
|
|
|
|
contributions = ProposalContribution.query.filter_by(proposal_id=proposal_id).all()
|
|
|
|
|
for c in contributions:
|
|
|
|
|
send_email(c.user.email_address, 'contribution_update', {
|
|
|
|
|
'proposal': g.current_proposal,
|
|
|
|
|
'proposal_update': update,
|
|
|
|
|
'preview': preview,
|
|
|
|
|
'update_url': make_url(f'/proposals/{proposal_id}?tab=updates&update={update.id}'),
|
|
|
|
|
})
|
|
|
|
|
|
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"])
|
|
|
|
|
@requires_team_member_auth
|
|
|
|
|
@endpoint.api(
|
|
|
|
|
parameter('address', type=str, required=True)
|
|
|
|
|
)
|
|
|
|
|
def post_proposal_team_invite(proposal_id, address):
|
|
|
|
|
invite = ProposalTeamInvite(
|
|
|
|
|
proposal_id=proposal_id,
|
|
|
|
|
address=address
|
|
|
|
|
)
|
|
|
|
|
db.session.add(invite)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
# Send email
|
|
|
|
|
# TODO: Move this to some background task / after request action
|
|
|
|
|
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
|
|
|
|
|
@endpoint.api()
|
|
|
|
|
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()
|
2018-11-26 17:14:00 -08:00
|
|
|
|
return None, 202
|
|
|
|
|
|
2018-11-21 19:18:22 -08:00
|
|
|
|
|
|
|
|
|
@blueprint.route("/<proposal_id>/contributions", methods=["GET"])
|
|
|
|
|
@endpoint.api()
|
|
|
|
|
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-23 07:00:30 -08:00
|
|
|
|
|
2019-01-09 12:48:41 -08:00
|
|
|
|
top_contributions = ProposalContribution.query \
|
|
|
|
|
.filter_by(proposal_id=proposal_id, status=CONFIRMED) \
|
|
|
|
|
.order_by(ProposalContribution.amount.desc()) \
|
|
|
|
|
.limit(5) \
|
|
|
|
|
.all()
|
|
|
|
|
latest_contributions = ProposalContribution.query \
|
|
|
|
|
.filter_by(proposal_id=proposal_id, status=CONFIRMED) \
|
|
|
|
|
.order_by(ProposalContribution.date_created.desc()) \
|
|
|
|
|
.limit(5) \
|
|
|
|
|
.all()
|
2019-01-23 07:00:30 -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"])
|
|
|
|
|
@endpoint.api()
|
|
|
|
|
def get_proposal_contribution(proposal_id, contribution_id):
|
|
|
|
|
proposal = Proposal.query.filter_by(id=proposal_id).first()
|
|
|
|
|
if proposal:
|
2019-01-08 09:44:54 -08:00
|
|
|
|
contribution = ProposalContribution.query.filter_by(id=contribution_id).first()
|
2018-11-21 19:18:22 -08:00
|
|
|
|
if contribution:
|
|
|
|
|
return proposal_contribution_schema.dump(contribution)
|
|
|
|
|
else:
|
|
|
|
|
return {"message": "No contribution matching id"}
|
|
|
|
|
else:
|
|
|
|
|
return {"message": "No proposal matching id"}, 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@blueprint.route("/<proposal_id>/contributions", methods=["POST"])
|
2018-12-14 11:36:22 -08:00
|
|
|
|
@requires_auth
|
2018-11-21 19:18:22 -08:00
|
|
|
|
@endpoint.api(
|
|
|
|
|
parameter('amount', type=str, required=True)
|
|
|
|
|
)
|
2019-01-06 14:48:07 -08:00
|
|
|
|
def post_proposal_contribution(proposal_id, amount):
|
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
|
|
|
|
|
contribution = ProposalContribution \
|
2019-01-09 11:35:37 -08:00
|
|
|
|
.get_existing_contribution(g.current_user.id, proposal_id, amount)
|
2019-01-06 22:58:33 -08:00
|
|
|
|
|
|
|
|
|
if not contribution:
|
|
|
|
|
code = 201
|
|
|
|
|
contribution = ProposalContribution(
|
|
|
|
|
proposal_id=proposal_id,
|
|
|
|
|
user_id=g.current_user.id,
|
|
|
|
|
amount=amount
|
|
|
|
|
)
|
|
|
|
|
db.session.add(contribution)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
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
|
|
|
|
|
@endpoint.api(
|
|
|
|
|
parameter('to', type=str, required=True),
|
|
|
|
|
parameter('amount', type=str, required=True),
|
|
|
|
|
parameter('txid', type=str, required=True),
|
|
|
|
|
)
|
|
|
|
|
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:
|
|
|
|
|
# TODO: Log in sentry
|
|
|
|
|
print(f'Unknown contribution {contribution_id} confirmed with txid {txid}')
|
|
|
|
|
return {"message": "No contribution matching id"}, 404
|
|
|
|
|
|
2019-01-16 14:26:45 -08:00
|
|
|
|
if contribution.status == CONFIRMED:
|
|
|
|
|
# Duplicates can happen, just return ok
|
|
|
|
|
return None, 200
|
|
|
|
|
|
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)
|
|
|
|
|
db.session.commit()
|
2019-01-16 14:26:45 -08:00
|
|
|
|
|
|
|
|
|
# Send to the user
|
|
|
|
|
send_email(contribution.user.email_address, 'contribution_confirmed', {
|
|
|
|
|
'contribution': contribution,
|
|
|
|
|
'proposal': contribution.proposal,
|
|
|
|
|
'tx_explorer_url': f'https://explorer.zcha.in/transactions/{txid}',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# 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.get_amount_funded(),
|
|
|
|
|
'proposal_url': make_url(f'/proposals/{contribution.proposal.id}'),
|
|
|
|
|
'contributor_url': make_url(f'/profile/{contribution.user.id}'),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# TODO: Once we have a task queuer in place, queue emails to everyone
|
|
|
|
|
# on funding target reached.
|
|
|
|
|
|
2019-01-08 14:58:32 -08:00
|
|
|
|
return None, 200
|
2019-01-09 13:32:51 -08:00
|
|
|
|
|
2019-01-23 07:00:30 -08:00
|
|
|
|
|
2019-01-09 13:32:51 -08:00
|
|
|
|
@blueprint.route("/contribution/<contribution_id>", methods=["DELETE"])
|
|
|
|
|
@requires_auth
|
|
|
|
|
@endpoint.api()
|
|
|
|
|
def delete_proposal_contribution(contribution_id):
|
|
|
|
|
contribution = contribution = ProposalContribution.query.filter_by(
|
|
|
|
|
id=contribution_id).first()
|
|
|
|
|
if not contribution:
|
|
|
|
|
return {"message": "No contribution matching id"}, 404
|
|
|
|
|
|
|
|
|
|
if contribution.status == CONFIRMED:
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
contribution.status = DELETED
|
|
|
|
|
db.session.add(contribution)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
return None, 202
|