2019-02-19 09:20:17 -08:00
from datetime import datetime
2019-03-01 12:11:03 -08:00
from decimal import Decimal
from functools import reduce
from flask import Blueprint , request
from marshmallow import fields , validate
2019-03-12 10:08:16 -07:00
from sqlalchemy import func , or_ , text
2019-02-17 08:52:35 -08:00
2019-03-01 12:11:03 -08:00
import grant . utils . admin as admin
import grant . utils . auth as auth
2019-02-18 13:31:20 -08:00
from grant . comment . models import Comment , user_comments_schema , admin_comments_schema , admin_comment_schema
2019-02-06 12:56:21 -08:00
from grant . email . send import generate_email , send_email
2018-10-30 09:35:47 -07:00
from grant . extensions import db
2019-03-01 12:11:03 -08:00
from grant . milestone . models import Milestone
from grant . parser import body , query , paginated_fields
2019-01-16 21:01:29 -08:00
from grant . proposal . models import (
Proposal ,
2019-02-09 18:58:40 -08:00
ProposalArbiter ,
2019-01-16 21:01:29 -08:00
ProposalContribution ,
proposals_schema ,
proposal_schema ,
user_proposal_contributions_schema ,
2019-02-17 08:52:35 -08:00
admin_proposal_contribution_schema ,
2019-02-19 09:13:13 -08:00
admin_proposal_contributions_schema ,
2019-01-16 21:01:29 -08:00
)
2019-02-01 11:13:30 -08:00
from grant . rfp . models import RFP , admin_rfp_schema , admin_rfps_schema
2019-03-01 12:11:03 -08:00
from grant . settings import EXPLORER_URL
from grant . user . models import User , UserSettings , admin_users_schema , admin_user_schema
from grant . utils import pagination
from grant . utils . enums import Category
2019-02-14 16:08:18 -08:00
from grant . utils . enums import (
ProposalStatus ,
ProposalStage ,
ContributionStatus ,
ProposalArbiterStatus ,
MilestoneStage ,
RFPStatus ,
)
2019-03-01 12:11:03 -08:00
from grant . utils . misc import make_url
2019-01-23 07:00:30 -08:00
from . example_emails import example_email_args
2018-10-30 09:35:47 -07:00
blueprint = Blueprint ( ' admin ' , __name__ , url_prefix = ' /api/v1/admin ' )
2019-02-21 17:39:37 -08:00
def make_2fa_state ( ) :
2019-02-20 14:35:13 -08:00
return {
2019-02-21 17:39:37 -08:00
" isLoginFresh " : admin . is_auth_fresh ( ) ,
" has2fa " : admin . has_2fa_setup ( ) ,
2019-02-21 14:23:46 -08:00
" is2faAuthed " : admin . admin_is_2fa_authed ( ) ,
2019-02-21 17:39:37 -08:00
" backupCodeCount " : admin . backup_code_count ( ) ,
" isEmailVerified " : auth . is_email_verified ( ) ,
2019-02-20 14:35:13 -08:00
}
2018-10-30 09:35:47 -07:00
2019-02-21 17:39:37 -08:00
def make_login_state ( ) :
return {
" isLoggedIn " : admin . admin_is_authed ( ) ,
" is2faAuthed " : admin . admin_is_2fa_authed ( )
}
2018-10-30 09:35:47 -07:00
@blueprint.route ( " /checklogin " , methods = [ " GET " ] )
def loggedin ( ) :
2019-02-21 17:39:37 -08:00
return make_login_state ( )
2018-10-30 09:35:47 -07:00
@blueprint.route ( " /login " , methods = [ " POST " ] )
2019-03-01 12:11:03 -08:00
@body ( {
" username " : fields . Str ( required = False , missing = None ) ,
" password " : fields . Str ( required = False , missing = None )
} )
2018-10-30 09:35:47 -07:00
def login ( username , password ) :
2019-02-21 14:23:46 -08:00
if auth . auth_user ( username , password ) :
2019-02-21 16:39:41 -08:00
if admin . admin_is_authed ( ) :
2019-02-21 17:39:37 -08:00
return make_login_state ( )
2019-02-21 16:39:41 -08:00
return { " message " : " Username or password incorrect. " } , 401
2018-10-30 09:35:47 -07:00
2019-02-21 14:23:46 -08:00
@blueprint.route ( " /refresh " , methods = [ " POST " ] )
2019-03-01 12:11:03 -08:00
@body ( {
" password " : fields . Str ( required = True )
} )
2019-02-21 14:23:46 -08:00
def refresh ( password ) :
if auth . refresh_auth ( password ) :
2019-02-21 17:39:37 -08:00
return make_login_state ( )
2018-10-30 09:35:47 -07:00
else :
return { " message " : " Username or password incorrect. " } , 401
2019-02-21 14:23:46 -08:00
@blueprint.route ( " /2fa " , methods = [ " GET " ] )
def get_2fa ( ) :
if not admin . admin_is_authed ( ) :
return { " message " : " Must be authenticated " } , 403
return make_2fa_state ( )
@blueprint.route ( " /2fa/init " , methods = [ " GET " ] )
def get_2fa_init ( ) :
2019-02-21 17:39:37 -08:00
admin . throw_on_2fa_not_allowed ( )
2019-02-21 14:23:46 -08:00
return admin . make_2fa_setup ( )
@blueprint.route ( " /2fa/enable " , methods = [ " POST " ] )
2019-03-01 12:11:03 -08:00
@body ( {
" backupCodes " : fields . List ( fields . Str ( ) , required = True ) ,
" totpSecret " : fields . Str ( required = True ) ,
" verifyCode " : fields . Str ( required = True )
} )
2019-02-21 14:23:46 -08:00
def post_2fa_enable ( backup_codes , totp_secret , verify_code ) :
2019-02-21 17:39:37 -08:00
admin . throw_on_2fa_not_allowed ( )
2019-02-21 14:23:46 -08:00
admin . check_and_set_2fa_setup ( backup_codes , totp_secret , verify_code )
db . session . commit ( )
return make_2fa_state ( )
@blueprint.route ( " /2fa/verify " , methods = [ " POST " ] )
2019-03-01 12:11:03 -08:00
@body ( {
" verifyCode " : fields . Str ( required = True )
} )
2019-02-21 14:23:46 -08:00
def post_2fa_verify ( verify_code ) :
2019-02-22 09:55:45 -08:00
admin . throw_on_2fa_not_allowed ( allow_stale = True )
2019-02-21 14:23:46 -08:00
admin . admin_auth_2fa ( verify_code )
db . session . commit ( )
return make_2fa_state ( )
2018-10-30 09:35:47 -07:00
@blueprint.route ( " /logout " , methods = [ " GET " ] )
def logout ( ) :
2019-02-21 14:23:46 -08:00
admin . logout ( )
2019-02-20 14:35:13 -08:00
return {
" isLoggedIn " : False ,
" is2faAuthed " : False
}
2018-10-30 09:35:47 -07:00
@blueprint.route ( " /stats " , methods = [ " GET " ] )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2018-10-30 09:35:47 -07:00
def stats ( ) :
user_count = db . session . query ( func . count ( User . id ) ) . scalar ( )
proposal_count = db . session . query ( func . count ( Proposal . id ) ) . scalar ( )
2019-01-09 10:23:08 -08:00
proposal_pending_count = db . session . query ( func . count ( Proposal . id ) ) \
2019-01-30 09:59:15 -08:00
. filter ( Proposal . status == ProposalStatus . PENDING ) \
2019-01-09 10:23:08 -08:00
. scalar ( )
2019-02-05 12:45:26 -08:00
proposal_no_arbiter_count = db . session . query ( func . count ( Proposal . id ) ) \
2019-02-11 13:59:29 -08:00
. join ( Proposal . arbiter ) \
2019-02-05 12:45:26 -08:00
. filter ( Proposal . status == ProposalStatus . LIVE ) \
2019-02-09 18:58:40 -08:00
. filter ( ProposalArbiter . status == ProposalArbiterStatus . MISSING ) \
2019-02-05 12:45:26 -08:00
. scalar ( )
2019-02-13 08:54:46 -08:00
proposal_milestone_payouts_count = db . session . query ( func . count ( Proposal . id ) ) \
. join ( Proposal . milestones ) \
. filter ( Proposal . status == ProposalStatus . LIVE ) \
. filter ( Milestone . stage == MilestoneStage . ACCEPTED ) \
. scalar ( )
2019-02-17 08:52:35 -08:00
# Count contributions on proposals that didn't get funded for users who have specified a refund address
contribution_refundable_count = db . session . query ( func . count ( ProposalContribution . id ) ) \
2019-02-20 13:44:12 -08:00
. filter ( ProposalContribution . refund_tx_id == None ) \
2019-02-21 10:02:29 -08:00
. filter ( ProposalContribution . staking == False ) \
2019-03-13 16:36:24 -07:00
. filter ( ProposalContribution . no_refund == False ) \
2019-02-23 13:38:06 -08:00
. filter ( ProposalContribution . status == ContributionStatus . CONFIRMED ) \
2019-02-17 08:52:35 -08:00
. join ( Proposal ) \
2019-02-23 13:38:06 -08:00
. filter ( or_ (
2019-03-06 12:25:58 -08:00
Proposal . stage == ProposalStage . FAILED ,
Proposal . stage == ProposalStage . CANCELED ,
) ) \
2019-02-17 08:52:35 -08:00
. join ( ProposalContribution . user ) \
. join ( UserSettings ) \
. filter ( UserSettings . refund_address != None ) \
. scalar ( )
2018-10-30 09:35:47 -07:00
return {
" userCount " : user_count ,
2019-01-09 10:23:08 -08:00
" proposalCount " : proposal_count ,
" proposalPendingCount " : proposal_pending_count ,
2019-02-05 12:45:26 -08:00
" proposalNoArbiterCount " : proposal_no_arbiter_count ,
2019-02-13 08:54:46 -08:00
" proposalMilestonePayoutsCount " : proposal_milestone_payouts_count ,
2019-02-17 08:52:35 -08:00
" contributionRefundableCount " : contribution_refundable_count ,
2018-10-30 09:35:47 -07:00
}
2019-01-16 21:01:29 -08:00
# USERS
2019-02-04 13:18:50 -08:00
@blueprint.route ( ' /users/<user_id> ' , methods = [ ' DELETE ' ] )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-02-04 13:18:50 -08:00
def delete_user ( user_id ) :
user = User . query . filter ( User . id == user_id ) . first ( )
if not user :
return { " message " : " No user matching that id " } , 404
db . session . delete ( user )
db . session . commit ( )
2019-03-01 12:11:03 -08:00
return { " message " : " ok " } , 200
2018-10-30 09:35:47 -07:00
@blueprint.route ( " /users " , methods = [ " GET " ] )
2019-03-01 12:11:03 -08:00
@query ( paginated_fields )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-02-14 20:11:47 -08:00
def get_users ( page , filters , search , sort ) :
filters_workaround = request . args . getlist ( ' filters[] ' )
page = pagination . user (
schema = admin_users_schema ,
query = User . query ,
page = page ,
filters = filters_workaround ,
search = search ,
sort = sort ,
)
return page
2019-01-16 21:01:29 -08:00
@blueprint.route ( ' /users/<id> ' , methods = [ ' GET ' ] )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-01-16 21:01:29 -08:00
def get_user ( id ) :
user_db = User . query . filter ( User . id == id ) . first ( )
if user_db :
2019-02-06 06:31:38 -08:00
user = admin_user_schema . dump ( user_db )
2018-10-30 09:35:47 -07:00
user_proposals = Proposal . query . filter ( Proposal . team . any ( id = user [ ' userid ' ] ) ) . all ( )
user [ ' proposals ' ] = proposals_schema . dump ( user_proposals )
2019-01-16 21:01:29 -08:00
user_comments = Comment . get_by_user ( user_db )
user [ ' comments ' ] = user_comments_schema . dump ( user_comments )
contributions = ProposalContribution . get_by_userid ( user_db . id )
contributions_dump = user_proposal_contributions_schema . dump ( contributions )
user [ " contributions " ] = contributions_dump
return user
return { " message " : f " Could not find user with id { id } " } , 404
2019-02-14 20:11:47 -08:00
@blueprint.route ( ' /users/<user_id> ' , methods = [ ' PUT ' ] )
2019-03-01 12:11:03 -08:00
@body ( {
" silenced " : fields . Bool ( required = False , missing = None ) ,
" banned " : fields . Bool ( required = False , missing = None ) ,
" bannedReason " : fields . Str ( required = False , missing = None ) ,
" isAdmin " : fields . Bool ( required = False , missing = None ) ,
} )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-02-20 10:04:25 -08:00
def edit_user ( user_id , silenced , banned , banned_reason , is_admin ) :
2019-02-14 20:11:47 -08:00
user = User . query . filter ( User . id == user_id ) . first ( )
if not user :
return { " message " : f " Could not find user with id { id } " } , 404
if silenced is not None :
2019-02-20 10:04:25 -08:00
user . set_silenced ( silenced )
2019-02-14 20:11:47 -08:00
if banned is not None :
if banned and not banned_reason : # if banned true, provide reason
return { " message " : " Please include reason for banning " } , 417
2019-02-20 10:04:25 -08:00
user . set_banned ( banned , banned_reason )
if is_admin is not None :
user . set_admin ( is_admin )
2019-02-14 20:11:47 -08:00
db . session . commit ( )
return admin_user_schema . dump ( user )
2019-02-06 10:31:53 -08:00
# ARBITERS
@blueprint.route ( " /arbiters " , methods = [ " GET " ] )
2019-03-01 12:11:03 -08:00
@query ( {
" search " : fields . Str ( required = False , missing = None )
} )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-02-06 10:31:53 -08:00
def get_arbiters ( search ) :
results = [ ]
error = None
if len ( search ) < 3 :
error = ' search query must be at least 3 characters long '
else :
users = User . query . filter (
User . email_address . ilike ( f ' % { search } % ' ) | User . display_name . ilike ( f ' % { search } % ' )
2019-02-06 10:56:08 -08:00
) . order_by ( User . display_name ) . all ( )
2019-02-06 10:31:53 -08:00
results = admin_users_schema . dump ( users )
return {
' results ' : results ,
' search ' : search ,
' error ' : error
}
@blueprint.route ( ' /arbiters ' , methods = [ ' PUT ' ] )
2019-03-01 12:11:03 -08:00
@body ( {
" proposalId " : fields . Int ( required = True ) ,
" userId " : fields . Int ( required = True ) ,
} )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-02-06 10:31:53 -08:00
def set_arbiter ( proposal_id , user_id ) :
proposal = Proposal . query . filter ( Proposal . id == proposal_id ) . first ( )
if not proposal :
return { " message " : " Proposal not found " } , 404
2019-02-15 19:35:25 -08:00
for member in proposal . team :
if member . id == user_id :
return { " message " : " Cannot set proposal team member as arbiter " } , 400
if proposal . is_failed :
return { " message " : " Cannot set arbiter on failed proposal " } , 400
2019-02-06 10:31:53 -08:00
user = User . query . filter ( User . id == user_id ) . first ( )
if not user :
return { " message " : " User not found " } , 404
2019-02-09 18:58:40 -08:00
# send email
code = user . email_verification . code
send_email ( user . email_address , ' proposal_arbiter ' , {
' proposal ' : proposal ,
' proposal_url ' : make_url ( f ' /proposals/ { proposal . id } ' ) ,
' accept_url ' : make_url ( f ' /email/arbiter?code= { code } &proposalId= { proposal . id } ' ) ,
} )
proposal . arbiter . user = user
proposal . arbiter . status = ProposalArbiterStatus . NOMINATED
db . session . add ( proposal . arbiter )
db . session . commit ( )
2019-02-06 12:56:21 -08:00
2019-02-06 10:31:53 -08:00
return {
2019-03-06 12:25:58 -08:00
' proposal ' : proposal_schema . dump ( proposal ) ,
' user ' : admin_user_schema . dump ( user )
} , 200
2019-02-06 10:31:53 -08:00
2019-01-16 21:01:29 -08:00
# PROPOSALS
2018-10-30 09:35:47 -07:00
@blueprint.route ( " /proposals " , methods = [ " GET " ] )
2019-03-01 12:11:03 -08:00
@query ( paginated_fields )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-02-05 12:34:19 -08:00
def get_proposals ( page , filters , search , sort ) :
filters_workaround = request . args . getlist ( ' filters[] ' )
page = pagination . proposal (
schema = proposals_schema ,
query = Proposal . query ,
page = page ,
filters = filters_workaround ,
search = search ,
sort = sort ,
)
return page
2018-10-30 09:35:47 -07:00
2019-01-09 10:23:08 -08:00
@blueprint.route ( ' /proposals/<id> ' , methods = [ ' GET ' ] )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-01-09 10:23:08 -08:00
def get_proposal ( id ) :
proposal = Proposal . query . filter ( Proposal . id == id ) . first ( )
if proposal :
return proposal_schema . dump ( proposal )
2019-01-29 15:50:27 -08:00
return { " message " : f " Could not find proposal with id { id } " } , 404
2019-01-09 10:23:08 -08:00
2018-10-30 09:35:47 -07:00
@blueprint.route ( ' /proposals/<id> ' , methods = [ ' DELETE ' ] )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2018-10-30 09:35:47 -07:00
def delete_proposal ( id ) :
2018-11-01 18:35:14 -07:00
return { " message " : " Not implemented. " } , 400
2019-01-09 10:23:08 -08:00
2019-01-29 15:50:27 -08:00
@blueprint.route ( ' /proposals/<id> ' , methods = [ ' PUT ' ] )
2019-03-01 12:11:03 -08:00
@body ( {
2019-03-06 12:25:58 -08:00
" contributionMatching " : fields . Int ( required = False , missing = None ) ,
" contributionBounty " : fields . Str ( required = False , missing = None )
2019-03-01 12:11:03 -08:00
} )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-03-06 12:25:58 -08:00
def update_proposal ( id , contribution_matching , contribution_bounty ) :
2019-01-29 15:50:27 -08:00
proposal = Proposal . query . filter ( Proposal . id == id ) . first ( )
2019-02-23 13:38:06 -08:00
if not proposal :
return { " message " : f " Could not find proposal with id { id } " } , 404
2019-01-29 15:50:27 -08:00
2019-02-23 13:38:06 -08:00
if contribution_matching is not None :
proposal . set_contribution_matching ( contribution_matching )
2019-01-29 15:50:27 -08:00
2019-03-06 12:25:58 -08:00
if contribution_bounty is not None :
proposal . set_contribution_bounty ( contribution_bounty )
2019-02-23 13:38:06 -08:00
db . session . add ( proposal )
db . session . commit ( )
return proposal_schema . dump ( proposal )
2019-01-29 15:50:27 -08:00
2019-01-09 10:23:08 -08:00
@blueprint.route ( ' /proposals/<id>/approve ' , methods = [ ' PUT ' ] )
2019-03-01 12:11:03 -08:00
@body ( {
" isApprove " : fields . Bool ( required = True ) ,
" rejectReason " : fields . Str ( required = False , missing = None )
} )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-01-09 10:23:08 -08:00
def approve_proposal ( id , is_approve , reject_reason = None ) :
proposal = Proposal . query . filter_by ( id = id ) . first ( )
if proposal :
proposal . approve_pending ( is_approve , reject_reason )
db . session . commit ( )
return proposal_schema . dump ( proposal )
2019-02-13 08:54:46 -08:00
return { " message " : " No proposal found. " } , 404
2019-02-23 13:38:06 -08:00
@blueprint.route ( ' /proposals/<id>/cancel ' , methods = [ ' PUT ' ] )
@admin.admin_auth_required
def cancel_proposal ( id ) :
proposal = Proposal . query . filter_by ( id = id ) . first ( )
if not proposal :
return { " message " : " No proposal found. " } , 404
proposal . cancel ( )
db . session . add ( proposal )
db . session . commit ( )
return proposal_schema . dump ( proposal )
2019-02-13 08:54:46 -08:00
@blueprint.route ( " /proposals/<id>/milestone/<mid>/paid " , methods = [ " PUT " ] )
2019-03-01 12:11:03 -08:00
@body ( {
" txId " : fields . Str ( required = True ) ,
} )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-02-13 08:54:46 -08:00
def paid_milestone_payout_request ( id , mid , tx_id ) :
proposal = Proposal . query . filter_by ( id = id ) . first ( )
if not proposal :
return { " message " : " No proposal matching id " } , 404
if not proposal . is_funded :
return { " message " : " Proposal is not fully funded " } , 400
for ms in proposal . milestones :
if ms . id == int ( mid ) :
ms . mark_paid ( tx_id )
db . session . add ( ms )
db . session . flush ( )
2019-02-13 12:30:58 -08:00
# check if this is the final ms, and update proposal.stage
2019-02-13 08:54:46 -08:00
num_paid = reduce ( lambda a , x : a + ( 1 if x . stage == MilestoneStage . PAID else 0 ) , proposal . milestones , 0 )
if num_paid == len ( proposal . milestones ) :
proposal . stage = ProposalStage . COMPLETED # WIP -> COMPLETED
db . session . add ( proposal )
db . session . flush ( )
db . session . commit ( )
2019-02-13 12:30:58 -08:00
# email TEAM that payout request was PAID
amount = Decimal ( ms . payout_percent ) * Decimal ( proposal . target ) / 100
for member in proposal . team :
send_email ( member . email_address , ' milestone_paid ' , {
' proposal ' : proposal ,
2019-03-14 20:26:28 -07:00
' milestone ' : ms ,
2019-02-13 12:30:58 -08:00
' amount ' : amount ,
' tx_explorer_url ' : f ' { EXPLORER_URL } transactions/ { tx_id } ' ,
' proposal_milestones_url ' : make_url ( f ' /proposals/ { proposal . id } ?tab=milestones ' ) ,
} )
2019-02-13 08:54:46 -08:00
return proposal_schema . dump ( proposal ) , 200
return { " message " : " No milestone matching id " } , 404
2019-01-09 11:08:25 -08:00
2019-01-16 21:01:29 -08:00
# EMAIL
2019-01-09 11:08:25 -08:00
@blueprint.route ( ' /email/example/<type> ' , methods = [ ' GET ' ] )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-01-09 11:08:25 -08:00
def get_email_example ( type ) :
2019-01-16 14:26:45 -08:00
email = generate_email ( type , example_email_args . get ( type ) )
if email [ ' info ' ] . get ( ' subscription ' ) :
# Unserializable, so remove
email [ ' info ' ] . pop ( ' subscription ' , None )
return email
2019-01-30 09:59:15 -08:00
# Requests for Proposal
@blueprint.route ( ' /rfps ' , methods = [ ' GET ' ] )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-01-30 09:59:15 -08:00
def get_rfps ( ) :
rfps = RFP . query . all ( )
2019-02-01 11:13:30 -08:00
return admin_rfps_schema . dump ( rfps )
2019-01-30 09:59:15 -08:00
@blueprint.route ( ' /rfps ' , methods = [ ' POST ' ] )
2019-03-01 12:11:03 -08:00
@body ( {
" title " : fields . Str ( required = True ) ,
" brief " : fields . Str ( required = True ) ,
" content " : fields . Str ( required = True ) ,
" category " : fields . Str ( required = True , validate = validate . OneOf ( choices = Category . list ( ) ) ) ,
" bounty " : fields . Str ( required = False , missing = 0 ) ,
" matching " : fields . Bool ( required = False , missing = False ) ,
" dateCloses " : fields . Int ( required = True )
} )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-02-08 11:02:34 -08:00
def create_rfp ( date_closes , * * kwargs ) :
2019-01-30 09:59:15 -08:00
rfp = RFP (
2019-02-08 11:02:34 -08:00
* * kwargs ,
date_closes = datetime . fromtimestamp ( date_closes ) if date_closes else None ,
2019-01-30 09:59:15 -08:00
)
db . session . add ( rfp )
db . session . commit ( )
2019-03-01 12:11:03 -08:00
return admin_rfp_schema . dump ( rfp ) , 200
2019-01-30 09:59:15 -08:00
@blueprint.route ( ' /rfps/<rfp_id> ' , methods = [ ' GET ' ] )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-01-30 09:59:15 -08:00
def get_rfp ( rfp_id ) :
rfp = RFP . query . filter ( RFP . id == rfp_id ) . first ( )
if not rfp :
return { " message " : " No RFP matching that id " } , 404
2019-02-01 11:13:30 -08:00
return admin_rfp_schema . dump ( rfp )
2019-01-30 09:59:15 -08:00
@blueprint.route ( ' /rfps/<rfp_id> ' , methods = [ ' PUT ' ] )
2019-03-01 12:11:03 -08:00
@body ( {
" title " : fields . Str ( required = True ) ,
" brief " : fields . Str ( required = True ) ,
2019-03-14 13:29:02 -07:00
" status " : fields . Str ( required = True , validate = validate . OneOf ( choices = RFPStatus . list ( ) ) ) ,
2019-03-01 12:11:03 -08:00
" content " : fields . Str ( required = True ) ,
" category " : fields . Str ( required = True , validate = validate . OneOf ( choices = Category . list ( ) ) ) ,
2019-03-06 12:25:58 -08:00
" bounty " : fields . Str ( required = False , allow_none = True , missing = None ) ,
2019-03-04 10:52:57 -08:00
" matching " : fields . Bool ( required = False , default = False , missing = False ) ,
" dateCloses " : fields . Int ( required = False , missing = None ) ,
2019-03-01 12:11:03 -08:00
} )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-02-07 12:35:33 -08:00
def update_rfp ( rfp_id , title , brief , content , category , bounty , matching , date_closes , status ) :
2019-01-30 09:59:15 -08:00
rfp = RFP . query . filter ( RFP . id == rfp_id ) . first ( )
if not rfp :
return { " message " : " No RFP matching that id " } , 404
2019-02-07 12:35:33 -08:00
# Update fields
2019-01-30 09:59:15 -08:00
rfp . title = title
rfp . brief = brief
rfp . content = content
rfp . category = category
2019-02-07 12:35:33 -08:00
rfp . matching = matching
2019-03-06 12:25:58 -08:00
rfp . bounty = bounty
2019-02-08 08:54:20 -08:00
rfp . date_closes = datetime . fromtimestamp ( date_closes ) if date_closes else None
2019-02-07 12:35:33 -08:00
# Update timestamps if status changed
if rfp . status != status :
2019-02-08 11:02:34 -08:00
if status == RFPStatus . LIVE and not rfp . date_opened :
2019-02-07 12:35:33 -08:00
rfp . date_opened = datetime . now ( )
if status == RFPStatus . CLOSED :
rfp . date_closed = datetime . now ( )
rfp . status = status
2019-01-30 09:59:15 -08:00
db . session . add ( rfp )
db . session . commit ( )
2019-02-01 11:13:30 -08:00
return admin_rfp_schema . dump ( rfp )
2019-01-30 09:59:15 -08:00
@blueprint.route ( ' /rfps/<rfp_id> ' , methods = [ ' DELETE ' ] )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-01-30 09:59:15 -08:00
def delete_rfp ( rfp_id ) :
rfp = RFP . query . filter ( RFP . id == rfp_id ) . first ( )
if not rfp :
return { " message " : " No RFP matching that id " } , 404
db . session . delete ( rfp )
db . session . commit ( )
2019-03-01 12:11:03 -08:00
return { " message " : " ok " } , 200
2019-02-06 08:21:19 -08:00
# Contributions
@blueprint.route ( ' /contributions ' , methods = [ ' GET ' ] )
2019-03-01 12:11:03 -08:00
@query ( paginated_fields )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-02-06 08:21:19 -08:00
def get_contributions ( page , filters , search , sort ) :
filters_workaround = request . args . getlist ( ' filters[] ' )
page = pagination . contribution (
page = page ,
2019-02-19 09:13:13 -08:00
schema = admin_proposal_contributions_schema ,
2019-02-06 08:21:19 -08:00
filters = filters_workaround ,
search = search ,
sort = sort ,
)
return page
@blueprint.route ( ' /contributions ' , methods = [ ' POST ' ] )
2019-03-01 12:11:03 -08:00
@body ( {
" proposalId " : fields . Int ( required = True ) ,
" userId " : fields . Int ( required = True ) ,
2019-03-14 20:26:28 -07:00
" status " : fields . Str ( required = True , validate = validate . OneOf ( choices = ContributionStatus . list ( ) ) ) ,
2019-03-01 12:11:03 -08:00
" amount " : fields . Str ( required = True ) ,
" txId " : fields . Str ( required = False , missing = None )
} )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-02-06 08:21:19 -08:00
def create_contribution ( proposal_id , user_id , status , amount , tx_id ) :
2019-02-06 11:01:46 -08:00
# Some fields set manually since we're admin, and normally don't do this
2019-02-06 08:21:19 -08:00
contribution = ProposalContribution (
proposal_id = proposal_id ,
user_id = user_id ,
amount = amount ,
)
2019-02-06 11:01:46 -08:00
contribution . status = status
contribution . tx_id = tx_id
2019-02-06 08:21:19 -08:00
db . session . add ( contribution )
2019-02-15 19:35:25 -08:00
db . session . flush ( )
contribution . proposal . set_pending_when_ready ( )
contribution . proposal . set_funded_when_ready ( )
2019-02-06 08:21:19 -08:00
db . session . commit ( )
2019-02-17 08:52:35 -08:00
return admin_proposal_contribution_schema . dump ( contribution ) , 200
2019-02-06 08:21:19 -08:00
@blueprint.route ( ' /contributions/<contribution_id> ' , methods = [ ' GET ' ] )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-02-06 08:21:19 -08:00
def get_contribution ( contribution_id ) :
contribution = ProposalContribution . query . filter ( ProposalContribution . id == contribution_id ) . first ( )
if not contribution :
return { " message " : " No contribution matching that id " } , 404
2019-02-17 08:52:35 -08:00
return admin_proposal_contribution_schema . dump ( contribution ) , 200
2019-02-06 08:21:19 -08:00
@blueprint.route ( ' /contributions/<contribution_id> ' , methods = [ ' PUT ' ] )
2019-03-01 12:11:03 -08:00
@body ( {
" proposalId " : fields . Int ( required = False , missing = None ) ,
" userId " : fields . Int ( required = False , missing = None ) ,
2019-03-14 20:26:28 -07:00
" status " : fields . Str ( required = True , validate = validate . OneOf ( choices = ContributionStatus . list ( ) ) ) ,
2019-03-01 12:11:03 -08:00
" amount " : fields . Str ( required = False , missing = None ) ,
2019-03-06 12:25:58 -08:00
" txId " : fields . Str ( required = False , missing = None ) ,
" refundTxId " : fields . Str ( required = False , allow_none = True , missing = None ) ,
2019-03-01 12:11:03 -08:00
} )
2019-02-21 14:23:46 -08:00
@admin.admin_auth_required
2019-02-17 08:52:35 -08:00
def edit_contribution ( contribution_id , proposal_id , user_id , status , amount , tx_id , refund_tx_id ) :
2019-02-06 08:21:19 -08:00
contribution = ProposalContribution . query . filter ( ProposalContribution . id == contribution_id ) . first ( )
if not contribution :
return { " message " : " No contribution matching that id " } , 404
2019-02-19 09:13:13 -08:00
had_refund = contribution . refund_tx_id
2019-02-06 08:21:19 -08:00
2019-02-23 13:38:06 -08:00
# do not allow editing certain fields on contributions once a proposal has become funded
if ( proposal_id or user_id or status or amount or tx_id ) and contribution . proposal . is_funded :
2019-02-15 19:35:25 -08:00
return { " message " : " Cannot edit contributions to fully-funded proposals " } , 400
2019-02-06 08:21:19 -08:00
# Proposal ID (must belong to an existing proposal)
if proposal_id :
proposal = Proposal . query . filter ( Proposal . id == proposal_id ) . first ( )
if not proposal :
return { " message " : " No proposal matching that id " } , 400
contribution . proposal_id = proposal_id
2019-02-23 12:31:07 -08:00
# User ID (must belong to an existing user or 0 to unset)
if user_id is not None :
if user_id == 0 :
contribution . user_id = None
else :
user = User . query . filter ( User . id == user_id ) . first ( )
if not user :
return { " message " : " No user matching that id " } , 400
contribution . user_id = user_id
2019-02-06 08:21:19 -08:00
# Status (must be in list of statuses)
if status :
if not ContributionStatus . includes ( status ) :
return { " message " : " Invalid status " } , 400
contribution . status = status
# Amount (must be a Decimal parseable)
if amount :
try :
2019-02-06 14:24:07 -08:00
contribution . amount = str ( Decimal ( amount ) )
2019-02-06 08:21:19 -08:00
except :
return { " message " : " Amount could not be parsed as number " } , 400
# Transaction ID (no validation)
2019-02-21 10:21:46 -08:00
if tx_id is not None :
2019-02-06 08:21:19 -08:00
contribution . tx_id = tx_id
2019-02-17 08:52:35 -08:00
# Refund TX ID (no validation)
2019-02-21 10:21:46 -08:00
if refund_tx_id is not None :
2019-02-17 08:52:35 -08:00
contribution . refund_tx_id = refund_tx_id
2019-02-07 07:57:56 -08:00
2019-02-06 08:21:19 -08:00
db . session . add ( contribution )
2019-02-15 19:35:25 -08:00
db . session . flush ( )
contribution . proposal . set_pending_when_ready ( )
contribution . proposal . set_funded_when_ready ( )
2019-02-06 08:21:19 -08:00
db . session . commit ( )
2019-02-21 09:17:48 -08:00
return admin_proposal_contribution_schema . dump ( contribution ) , 200
2019-02-19 09:13:13 -08:00
2019-02-17 11:15:40 -08:00
# Comments
@blueprint.route ( ' /comments ' , methods = [ ' GET ' ] )
2019-03-14 13:29:02 -07:00
@body ( paginated_fields )
2019-02-21 16:56:00 -08:00
@admin.admin_auth_required
2019-02-17 11:15:40 -08:00
def get_comments ( page , filters , search , sort ) :
filters_workaround = request . args . getlist ( ' filters[] ' )
page = pagination . comment (
page = page ,
filters = filters_workaround ,
search = search ,
sort = sort ,
schema = admin_comments_schema
)
return page
2019-02-18 13:31:20 -08:00
@blueprint.route ( ' /comments/<comment_id> ' , methods = [ ' PUT ' ] )
2019-03-01 12:11:03 -08:00
@body ( {
" hidden " : fields . Bool ( required = False , missing = None ) ,
" reported " : fields . Bool ( required = False , missing = None ) ,
} )
2019-02-21 16:56:00 -08:00
@admin.admin_auth_required
2019-02-18 13:31:20 -08:00
def edit_comment ( comment_id , hidden , reported ) :
comment = Comment . query . filter ( Comment . id == comment_id ) . first ( )
if not comment :
return { " message " : " No comment matching that id " } , 404
if hidden is not None :
comment . hide ( hidden )
if reported is not None :
comment . report ( reported )
db . session . commit ( )
return admin_comment_schema . dump ( comment )
2019-03-12 10:08:16 -07:00
# Financials
@blueprint.route ( " /financials " , methods = [ " GET " ] )
@admin.admin_auth_required
def financials ( ) :
nfmt = ' 999999.99999999 ' # smallest unit of ZEC
def sql_pc ( where : str ) :
return f " SELECT SUM(TO_NUMBER(amount, ' { nfmt } ' )) FROM proposal_contribution WHERE { where } "
def sql_pc_p ( where : str ) :
return f '''
SELECT SUM ( TO_NUMBER ( amount , ' {nfmt} ' ) )
FROM proposal_contribution as pc
INNER JOIN proposal as p ON pc . proposal_id = p . id
WHERE { where }
'''
def sql_ms ( where : str ) :
return f '''
SELECT SUM ( TO_NUMBER ( ms . payout_percent , ' 999 ' ) / 100 * TO_NUMBER ( p . target , ' 999999.99999999 ' ) )
FROM milestone as ms
INNER JOIN proposal as p ON ms . proposal_id = p . id
WHERE { where }
'''
def ex ( sql : str ) :
res = db . engine . execute ( text ( sql ) )
return [ row [ 0 ] if row [ 0 ] else Decimal ( 0 ) for row in res ] [ 0 ] . normalize ( )
contributions = {
' total ' : str ( ex ( sql_pc ( " status = ' CONFIRMED ' AND staking = FALSE " ) ) ) ,
' staking ' : str ( ex ( sql_pc ( " status = ' CONFIRMED ' AND staking = TRUE " ) ) ) ,
' funding ' : str ( ex ( sql_pc_p ( " pc.status = ' CONFIRMED ' AND pc.staking = FALSE AND p.stage = ' FUNDING_REQUIRED ' " ) ) ) ,
' funded ' : str ( ex ( sql_pc_p ( " pc.status = ' CONFIRMED ' AND pc.staking = FALSE AND p.stage in ( ' WIP ' , ' COMPLETED ' ) " ) ) ) ,
' refunding ' : str ( ex ( sql_pc_p (
2019-03-13 16:36:24 -07:00
" pc.status = ' CONFIRMED ' AND pc.staking = FALSE AND pc.no_refund = FALSE AND pc.refund_tx_id IS NULL AND p.stage IN ( ' CANCELED ' , ' FAILED ' ) "
2019-03-12 10:08:16 -07:00
) ) ) ,
' refunded ' : str ( ex ( sql_pc_p (
2019-03-13 16:36:24 -07:00
" pc.status = ' CONFIRMED ' AND pc.staking = FALSE AND pc.no_refund = FALSE AND pc.refund_tx_id IS NOT NULL AND p.stage IN ( ' CANCELED ' , ' FAILED ' ) "
) ) ) ,
' donations ' : str ( ex ( sql_pc_p (
" (pc.status = ' CONFIRMED ' AND pc.staking = FALSE AND pc.refund_tx_id IS NULL) AND (pc.no_refund = TRUE OR pc.user_id IS NULL) AND p.stage IN ( ' CANCELED ' , ' FAILED ' ) "
2019-03-12 10:08:16 -07:00
) ) ) ,
' gross ' : str ( ex ( sql_pc_p ( " pc.status = ' CONFIRMED ' AND pc.refund_tx_id IS NULL " ) ) ) ,
}
po_due = ex ( sql_ms ( " ms.stage = ' ACCEPTED ' " ) ) # payments accepted but not yet marked as paid
po_paid = ex ( sql_ms ( " ms.stage = ' PAID ' " ) ) # will catch paid ms from all proposals regardless of status/stage
# expected payments
po_future = ex ( sql_ms ( " ms.stage IN ( ' IDLE ' , ' REJECTED ' , ' REQUESTED ' ) AND p.stage IN ( ' WIP ' , ' COMPLETED ' ) " ) )
po_total = po_due + po_paid + po_future
payouts = {
' total ' : str ( po_total ) ,
' due ' : str ( po_due ) ,
' paid ' : str ( po_paid ) ,
' future ' : str ( po_future ) ,
}
grants = {
' total ' : ' 0 ' ,
' matching ' : ' 0 ' ,
' bounty ' : ' 0 ' ,
}
def add_str_dec ( a : str , b : str ) :
return str ( Decimal ( a ) + Decimal ( b ) )
proposals = Proposal . query . all ( )
for p in proposals :
# CANCELED proposals excluded, though they could have had milestones paid out with grant funds
if p . stage in [ ProposalStage . WIP , ProposalStage . COMPLETED ] :
# matching
matching = Decimal ( p . contributed ) * Decimal ( p . contribution_matching )
remaining = Decimal ( p . target ) - Decimal ( p . contributed )
if matching > remaining :
matching = remaining
# bounty
bounty = Decimal ( p . contribution_bounty )
remaining = Decimal ( p . target ) - ( matching + Decimal ( p . contributed ) )
if bounty > remaining :
bounty = remaining
grants [ ' matching ' ] = add_str_dec ( grants [ ' matching ' ] , matching )
grants [ ' bounty ' ] = add_str_dec ( grants [ ' bounty ' ] , bounty )
grants [ ' total ' ] = add_str_dec ( grants [ ' total ' ] , matching + bounty )
return {
' grants ' : grants ,
' contributions ' : contributions ,
' payouts ' : payouts ,
' net ' : str ( Decimal ( contributions [ ' gross ' ] ) - Decimal ( payouts [ ' paid ' ] ) )
}