setup 'FUNDING BY ZOMG'

This commit is contained in:
Daniel Ternyak 2021-02-01 19:32:12 -06:00
parent 1a38eea631
commit 7f065b4163
No known key found for this signature in database
GPG Key ID: DF212D2DC5D0E245
15 changed files with 372 additions and 142 deletions

32
.github/workflows/node.js.yml vendored Normal file
View File

@ -0,0 +1,32 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches:
- develop
- master
pull_request:
branches:
- develop
- master
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: cd frontend && yarn && && yarn run lint && yarn run tsc

32
.github/workflows/python-app.yml vendored Normal file
View File

@ -0,0 +1,32 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Python application
on:
push:
branches:
- develop
- master
pull_request:
branches:
- develop
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Install dependencies
run: |
python -m pip install --upgrade pip
cd backend && pip install -r requirements/dev.txt
- name: Test with flask test
run: |
cd backend && cp .env.example .env && flask test

View File

@ -2,27 +2,11 @@ import React from 'react';
import BN from 'bn.js'; import BN from 'bn.js';
import { view } from 'react-easy-state'; import { view } from 'react-easy-state';
import { RouteComponentProps, withRouter } from 'react-router'; import { RouteComponentProps, withRouter } from 'react-router';
import { import { Alert, Button, Card, Col, Collapse, Input, message, Popconfirm, Row, Switch, Tag } from 'antd';
Alert,
Button,
Card,
Col,
Collapse,
Input,
message,
Popconfirm,
Row,
Tag,
} from 'antd';
import TextArea from 'antd/lib/input/TextArea'; import TextArea from 'antd/lib/input/TextArea';
import store from 'src/store'; import store from 'src/store';
import { formatDateSeconds, formatDurationSeconds } from 'util/time'; import { formatDateSeconds, formatDurationSeconds } from 'util/time';
import { import { MILESTONE_STAGE, PROPOSAL_ARBITER_STATUS, PROPOSAL_STAGE, PROPOSAL_STATUS } from 'src/types';
MILESTONE_STAGE,
PROPOSAL_ARBITER_STATUS,
PROPOSAL_STAGE,
PROPOSAL_STATUS,
} from 'src/types';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Back from 'components/Back'; import Back from 'components/Back';
import Markdown from 'components/Markdown'; import Markdown from 'components/Markdown';
@ -30,6 +14,7 @@ import ArbiterControl from 'components/ArbiterControl';
import { fromZat, toZat } from 'src/util/units'; import { fromZat, toZat } from 'src/util/units';
import FeedbackModal from '../FeedbackModal'; import FeedbackModal from '../FeedbackModal';
import { formatUsd } from 'util/formatters'; import { formatUsd } from 'util/formatters';
import './index.less'; import './index.less';
type Props = RouteComponentProps<any>; type Props = RouteComponentProps<any>;
@ -58,6 +43,8 @@ class ProposalDetailNaked extends React.Component<Props, State> {
return 'loading proposal...'; return 'loading proposal...';
} }
console.log(p.fundedByZomg);
const needsArbiter = const needsArbiter =
PROPOSAL_ARBITER_STATUS.MISSING === p.arbiter.status && PROPOSAL_ARBITER_STATUS.MISSING === p.arbiter.status &&
p.status === PROPOSAL_STATUS.LIVE && p.status === PROPOSAL_STATUS.LIVE &&
@ -94,9 +81,9 @@ class ProposalDetailNaked extends React.Component<Props, State> {
</p> </p>
) )
} }
placement="left" placement='left'
cancelText="cancel" cancelText='cancel'
okText="confirm" okText='confirm'
visible={this.state.showCancelAndRefundPopover} visible={this.state.showCancelAndRefundPopover}
okButtonProps={{ okButtonProps={{
loading: store.proposalDetailCanceling, loading: store.proposalDetailCanceling,
@ -105,8 +92,8 @@ class ProposalDetailNaked extends React.Component<Props, State> {
onConfirm={this.handleConfirmCancel} onConfirm={this.handleConfirmCancel}
> >
<Button <Button
icon="close-circle" icon='close-circle'
className="ProposalDetail-controls-control" className='ProposalDetail-controls-control'
loading={store.proposalDetailCanceling} loading={store.proposalDetailCanceling}
onClick={this.handleCancelAndRefundClick} onClick={this.handleCancelAndRefundClick}
disabled={disabled} disabled={disabled}
@ -128,9 +115,9 @@ class ProposalDetailNaked extends React.Component<Props, State> {
with funding? This cannot be undone. with funding? This cannot be undone.
</p> </p>
} }
placement="left" placement='left'
cancelText="cancel" cancelText='cancel'
okText="confirm" okText='confirm'
visible={this.state.showChangeToAcceptedWithFundingPopover} visible={this.state.showChangeToAcceptedWithFundingPopover}
okButtonProps={{ okButtonProps={{
loading: store.proposalDetailCanceling, loading: store.proposalDetailCanceling,
@ -139,8 +126,8 @@ class ProposalDetailNaked extends React.Component<Props, State> {
onConfirm={this.handleChangeToAcceptWithFundingConfirm} onConfirm={this.handleChangeToAcceptWithFundingConfirm}
> >
<Button <Button
icon="close-circle" icon='close-circle'
className="ProposalDetail-controls-control" className='ProposalDetail-controls-control'
loading={store.proposalDetailChangingToAcceptedWithFunding} loading={store.proposalDetailChangingToAcceptedWithFunding}
onClick={this.handleChangeToAcceptedWithFunding} onClick={this.handleChangeToAcceptedWithFunding}
block block
@ -170,7 +157,7 @@ class ProposalDetailNaked extends React.Component<Props, State> {
p.status === PROPOSAL_STATUS.APPROVED && ( p.status === PROPOSAL_STATUS.APPROVED && (
<Alert <Alert
showIcon showIcon
type="success" type='success'
message={`Approved on ${formatDateSeconds(p.dateApproved)}`} message={`Approved on ${formatDateSeconds(p.dateApproved)}`}
description={` description={`
This proposal has been approved and will become live when a team-member This proposal has been approved and will become live when a team-member
@ -206,25 +193,25 @@ class ProposalDetailNaked extends React.Component<Props, State> {
<Col span={isVersionTwo ? 16 : 24}> <Col span={isVersionTwo ? 16 : 24}>
<Alert <Alert
showIcon showIcon
type="warning" type='warning'
message="Review Discussion" message='Review Discussion'
description={ description={
<div> <div>
<p>Please review this proposal and render your judgment.</p> <p>Please review this proposal and render your judgment.</p>
<Button <Button
className="ProposalDetail-review" className='ProposalDetail-review'
loading={store.proposalDetailApprovingDiscussion} loading={store.proposalDetailApprovingDiscussion}
icon="check" icon='check'
type="primary" type='primary'
onClick={() => this.handleApproveDiscussion()} onClick={() => this.handleApproveDiscussion()}
> >
Open for Public Review Open for Public Review
</Button> </Button>
<Button <Button
className="ProposalDetail-review" className='ProposalDetail-review'
loading={store.proposalDetailApprovingDiscussion} loading={store.proposalDetailApprovingDiscussion}
icon="warning" icon='warning'
type="default" type='default'
onClick={() => { onClick={() => {
FeedbackModal.open({ FeedbackModal.open({
title: 'Request changes to this proposal?', title: 'Request changes to this proposal?',
@ -237,10 +224,10 @@ class ProposalDetailNaked extends React.Component<Props, State> {
Request Changes Request Changes
</Button> </Button>
<Button <Button
className="ProposalDetail-review" className='ProposalDetail-review'
loading={store.proposalDetailRejectingPermanently} loading={store.proposalDetailRejectingPermanently}
icon="close" icon='close'
type="danger" type='danger'
onClick={() => { onClick={() => {
FeedbackModal.open({ FeedbackModal.open({
title: 'Reject this proposal permanently?', title: 'Reject this proposal permanently?',
@ -269,27 +256,27 @@ class ProposalDetailNaked extends React.Component<Props, State> {
<Col span={isVersionTwo ? 16 : 24}> <Col span={isVersionTwo ? 16 : 24}>
<Alert <Alert
showIcon showIcon
type="warning" type='warning'
message="Review Pending" message='Review Pending'
description={ description={
<div> <div>
<p>Please review this proposal and render your judgment.</p> <p>Please review this proposal and render your judgment.</p>
<> <>
<Button <Button
className="ProposalDetail-review" className='ProposalDetail-review'
loading={store.proposalDetailAcceptingProposal} loading={store.proposalDetailAcceptingProposal}
icon="check" icon='check'
type="primary" type='primary'
onClick={() => this.handleAcceptProposal(true, true)} onClick={() => this.handleAcceptProposal(true, true)}
> >
Approve With Funding Approve With Funding
</Button> </Button>
<Button <Button
className="ProposalDetail-review" className='ProposalDetail-review'
loading={store.proposalDetailAcceptingProposal} loading={store.proposalDetailAcceptingProposal}
icon="check" icon='check'
type="default" type='default'
onClick={() => this.handleAcceptProposal(true, false)} onClick={() => this.handleAcceptProposal(true, false)}
> >
Approve Without Funding Approve Without Funding
@ -297,10 +284,10 @@ class ProposalDetailNaked extends React.Component<Props, State> {
</> </>
<Button <Button
className="ProposalDetail-review" className='ProposalDetail-review'
loading={store.proposalDetailMarkingChangesAsResolved} loading={store.proposalDetailMarkingChangesAsResolved}
icon="close" icon='close'
type="danger" type='danger'
onClick={() => { onClick={() => {
FeedbackModal.open({ FeedbackModal.open({
title: 'Request changes to this proposal?', title: 'Request changes to this proposal?',
@ -325,8 +312,8 @@ class ProposalDetailNaked extends React.Component<Props, State> {
p.status === PROPOSAL_STATUS.REJECTED && ( p.status === PROPOSAL_STATUS.REJECTED && (
<Alert <Alert
showIcon showIcon
type="error" type='error'
message="Changes requested" message='Changes requested'
description={ description={
<div> <div>
<p> <p>
@ -346,8 +333,8 @@ class ProposalDetailNaked extends React.Component<Props, State> {
p.changesRequestedDiscussion && ( p.changesRequestedDiscussion && (
<Alert <Alert
showIcon showIcon
type="error" type='error'
message="Changes requested" message='Changes requested'
description={ description={
<div> <div>
<p> <p>
@ -360,10 +347,10 @@ class ProposalDetailNaked extends React.Component<Props, State> {
<br /> <br />
<div style={{ display: 'flex', justifyContent: 'flex-end' }}> <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button <Button
className="ProposalDetail-review" className='ProposalDetail-review'
loading={false} loading={false}
icon="check" icon='check'
type="danger" type='danger'
onClick={this.handleMarkChangesAsResolved} onClick={this.handleMarkChangesAsResolved}
> >
Mark Request as Resolved Mark Request as Resolved
@ -381,8 +368,8 @@ class ProposalDetailNaked extends React.Component<Props, State> {
{!p.kycApproved ? ( {!p.kycApproved ? (
<Alert <Alert
showIcon showIcon
type="error" type='error'
message="KYC approval required" message='KYC approval required'
description={ description={
<div> <div>
<p> <p>
@ -390,10 +377,10 @@ class ProposalDetailNaked extends React.Component<Props, State> {
with payouts. with payouts.
</p> </p>
<Button <Button
className="ProposalDetail-review" className='ProposalDetail-review'
loading={store.proposalDetailApprovingKyc} loading={store.proposalDetailApprovingKyc}
icon="check" icon='check'
type="primary" type='primary'
onClick={() => this.handleApproveKYC()} onClick={() => this.handleApproveKYC()}
> >
KYC Approved KYC Approved
@ -404,8 +391,8 @@ class ProposalDetailNaked extends React.Component<Props, State> {
) : ( ) : (
<Alert <Alert
showIcon showIcon
type="warning" type='warning'
message="No arbiter on live proposal" message='No arbiter on live proposal'
description={ description={
<div> <div>
<p>An arbiter is required to review milestone payout requests.</p> <p>An arbiter is required to review milestone payout requests.</p>
@ -422,8 +409,8 @@ class ProposalDetailNaked extends React.Component<Props, State> {
p.status === PROPOSAL_STATUS.LIVE && ( p.status === PROPOSAL_STATUS.LIVE && (
<Alert <Alert
showIcon showIcon
type="info" type='info'
message="Arbiter has been nominated" message='Arbiter has been nominated'
description={ description={
<div> <div>
<p> <p>
@ -469,9 +456,9 @@ class ProposalDetailNaked extends React.Component<Props, State> {
return ( return (
<Alert <Alert
className="ProposalDetail-alert" className='ProposalDetail-alert'
showIcon showIcon
type="warning" type='warning'
message={null} message={null}
description={ description={
<div> <div>
@ -487,9 +474,9 @@ class ProposalDetailNaked extends React.Component<Props, State> {
</p>{' '} </p>{' '}
<pre>{p.payoutAddress}</pre> <pre>{p.payoutAddress}</pre>
<Input.Search <Input.Search
placeholder="please enter payment txid" placeholder='please enter payment txid'
value={this.state.paidTxId} value={this.state.paidTxId}
enterButton="Mark Paid" enterButton='Mark Paid'
onChange={e => this.setState({ paidTxId: e.target.value })} onChange={e => this.setState({ paidTxId: e.target.value })}
onSearch={this.handlePaidMilestone} onSearch={this.handlePaidMilestone}
/> />
@ -503,7 +490,7 @@ class ProposalDetailNaked extends React.Component<Props, State> {
p.isFailed && ( p.isFailed && (
<Alert <Alert
showIcon showIcon
type="error" type='error'
message={ message={
p.stage === PROPOSAL_STAGE.FAILED ? 'Proposal failed' : 'Proposal canceled' p.stage === PROPOSAL_STAGE.FAILED ? 'Proposal failed' : 'Proposal canceled'
} }
@ -525,17 +512,16 @@ class ProposalDetailNaked extends React.Component<Props, State> {
); );
const renderDeetItem = (name: string, val: any) => ( const renderDeetItem = (name: string, val: any) => (
<div className="ProposalDetail-deet"> <div className='ProposalDetail-deet'>
<span>{name}</span> <span>{name}</span>
{val} &nbsp; {val} &nbsp;
</div> </div>
); );
console.log(p); // @ts-ignore
return ( return (
<div className="ProposalDetail"> <div className='ProposalDetail'>
<Back to="/proposals" text="Proposals" /> <Back to='/proposals' text='Proposals' />
<h1>{p.title}</h1> <h1>{p.title}</h1>
<Row gutter={16}> <Row gutter={16}>
{/* MAIN */} {/* MAIN */}
@ -550,22 +536,22 @@ class ProposalDetailNaked extends React.Component<Props, State> {
{renderMilestoneAccepted()} {renderMilestoneAccepted()}
{renderFailed()} {renderFailed()}
<Collapse defaultActiveKey={['brief', 'content', 'milestones']}> <Collapse defaultActiveKey={['brief', 'content', 'milestones']}>
<Collapse.Panel key="brief" header="brief"> <Collapse.Panel key='brief' header='brief'>
{p.brief} {p.brief}
</Collapse.Panel> </Collapse.Panel>
<Collapse.Panel key="content" header="content"> <Collapse.Panel key='content' header='content'>
<Markdown source={p.content} /> <Markdown source={p.content} />
</Collapse.Panel> </Collapse.Panel>
<Collapse.Panel key="milestones" header="milestones"> <Collapse.Panel key='milestones' header='milestones'>
{p.milestones.map((milestone, i) => ( {p.milestones.map((milestone, i) => (
<Card <Card
title={ title={
<> <>
{milestone.title + ' '} {milestone.title + ' '}
{milestone.immediatePayout && ( {milestone.immediatePayout && (
<Tag color="magenta">Immediate Payout</Tag> <Tag color='magenta'>Immediate Payout</Tag>
)} )}
</> </>
} }
@ -590,7 +576,7 @@ class ProposalDetailNaked extends React.Component<Props, State> {
))} ))}
</Collapse.Panel> </Collapse.Panel>
<Collapse.Panel key="json" header="json"> <Collapse.Panel key='json' header='json'>
<pre>{JSON.stringify(p, null, 4)}</pre> <pre>{JSON.stringify(p, null, 4)}</pre>
</Collapse.Panel> </Collapse.Panel>
</Collapse> </Collapse>
@ -599,26 +585,38 @@ class ProposalDetailNaked extends React.Component<Props, State> {
{/* RIGHT SIDE */} {/* RIGHT SIDE */}
<Col span={6}> <Col span={6}>
{p.isVersionTwo && {p.isVersionTwo &&
!p.acceptedWithFunding && !p.acceptedWithFunding &&
p.stage === PROPOSAL_STAGE.WIP && ( p.stage === PROPOSAL_STAGE.WIP && (
<Alert <Alert
message="Accepted without funding" message='Accepted without funding'
description="This proposal has been posted publicly, but isn't being funded by the Zcash Foundation." description="This proposal has been posted publicly, but isn't being funded by the Zcash Foundation."
type="info" type='info'
showIcon showIcon
/> />
)} )}
{/* ACTIONS */} {/* ACTIONS */}
<Card size="small" className="ProposalDetail-controls"> <Card size='small' className='ProposalDetail-controls'>
{renderCancelControl()} {renderCancelControl()}
{renderArbiterControl()} {renderArbiterControl()}
{
p.acceptedWithFunding &&
<div style={{ marginTop: '10px' }}>
<Switch checkedChildren='Funded by ZOMG'
unCheckedChildren='Funded by ZF'
onChange={this.handleSwitchFunder}
loading={store.proposalDetailSwitchingFunder}
checked={p.fundedByZomg} />
</div>
}
{shouldShowChangeToAcceptedWithFunding && {shouldShowChangeToAcceptedWithFunding &&
renderChangeToAcceptedWithFundingControl()} renderChangeToAcceptedWithFundingControl()}
</Card> </Card>
{/* DETAILS */} {/* DETAILS */}
<Card title="Details" size="small"> <Card title='Details' size='small'>
{renderDeetItem('id', p.proposalId)} {renderDeetItem('id', p.proposalId)}
{renderDeetItem('created', formatDateSeconds(p.dateCreated))} {renderDeetItem('created', formatDateSeconds(p.dateCreated))}
{renderDeetItem( {renderDeetItem(
@ -630,10 +628,10 @@ class ProposalDetailNaked extends React.Component<Props, State> {
formatDurationSeconds(p.deadlineDuration), formatDurationSeconds(p.deadlineDuration),
)} )}
{p.datePublished && {p.datePublished &&
renderDeetItem( renderDeetItem(
'(deadline)', '(deadline)',
formatDateSeconds(p.datePublished + p.deadlineDuration), formatDateSeconds(p.datePublished + p.deadlineDuration),
)} )}
{renderDeetItem('isFailed', JSON.stringify(p.isFailed))} {renderDeetItem('isFailed', JSON.stringify(p.isFailed))}
{renderDeetItem('status', p.status)} {renderDeetItem('status', p.status)}
{renderDeetItem('stage', p.stage)} {renderDeetItem('stage', p.stage)}
@ -662,14 +660,14 @@ class ProposalDetailNaked extends React.Component<Props, State> {
</>, </>,
)} )}
{p.rfp && {p.rfp &&
renderDeetItem( renderDeetItem(
'rfp', 'rfp',
<Link to={`/rfps/${p.rfp.id}`}>{p.rfp.title}</Link>, <Link to={`/rfps/${p.rfp.id}`}>{p.rfp.title}</Link>,
)} )}
</Card> </Card>
{/* TEAM */} {/* TEAM */}
<Card title="Team" size="small"> <Card title='Team' size='small'>
{p.team.map(t => ( {p.team.map(t => (
<div key={t.userid}> <div key={t.userid}>
<Link to={`/users/${t.userid}`}>{t.displayName}</Link> <Link to={`/users/${t.userid}`}>{t.displayName}</Link>
@ -783,6 +781,10 @@ class ProposalDetailNaked extends React.Component<Props, State> {
await store.markMilestonePaid(pid, mid, this.state.paidTxId); await store.markMilestonePaid(pid, mid, this.state.paidTxId);
message.success('Marked milestone paid.'); message.success('Marked milestone paid.');
}; };
private handleSwitchFunder = async (checkValue: boolean) => {
store.switchProposalFunder(checkValue);
};
} }
const ProposalDetail = withRouter(view(ProposalDetailNaked)); const ProposalDetail = withRouter(view(ProposalDetailNaked));

View File

@ -142,6 +142,11 @@ async function approveDiscussion(
return data; return data;
} }
async function switchProposalFunder(id: number, fundedByZomg: boolean) {
const { data } = await api.put(`/admin/proposals/${id}/adjust-funder`, {fundedByZomg});
return data;
}
async function approveProposalKYC(id: number) { async function approveProposalKYC(id: number) {
const { data } = await api.put(`/admin/proposals/${id}/approve-kyc`); const { data } = await api.put(`/admin/proposals/${id}/approve-kyc`);
return data; return data;
@ -351,6 +356,7 @@ const app = store({
proposalDetailMarkingChangesAsResolved: false, proposalDetailMarkingChangesAsResolved: false,
proposalDetailAcceptingProposal: false, proposalDetailAcceptingProposal: false,
proposalDetailApprovingKyc: false, proposalDetailApprovingKyc: false,
proposalDetailSwitchingFunder: false,
proposalDetailMarkingMilestonePaid: false, proposalDetailMarkingMilestonePaid: false,
proposalDetailCanceling: false, proposalDetailCanceling: false,
proposalDetailUpdating: false, proposalDetailUpdating: false,
@ -695,6 +701,24 @@ const app = store({
} }
}, },
async switchProposalFunder(fundedByZomg: boolean) {
if (!app.proposalDetail) {
const m = 'store.acceptProposal(): Expected proposalDetail to be populated!';
app.generalError.push(m);
console.error(m);
return;
}
app.proposalDetailSwitchingFunder = true;
try {
const { proposalId } = app.proposalDetail;
const res = await switchProposalFunder(proposalId, fundedByZomg);
app.updateProposalInStore(res);
} catch (e) {
handleApiError(e);
}
app.proposalDetailSwitchingFunder = false;
},
async approveProposalKYC() { async approveProposalKYC() {
if (!app.proposalDetail) { if (!app.proposalDetail) {
const m = 'store.acceptProposal(): Expected proposalDetail to be populated!'; const m = 'store.acceptProposal(): Expected proposalDetail to be populated!';

View File

@ -124,6 +124,7 @@ export interface Proposal {
changesRequestedDiscussion: boolean | null; changesRequestedDiscussion: boolean | null;
changesRequestedDiscussionReason: string | null; changesRequestedDiscussionReason: string | null;
kycApproved: null | boolean; kycApproved: null | boolean;
fundedByZomg: boolean;
} }
export interface Comment { export interface Comment {
id: number; id: number;

View File

@ -4,7 +4,7 @@ from functools import reduce
from flask import Blueprint, request from flask import Blueprint, request
from marshmallow import fields, validate from marshmallow import fields, validate
from sqlalchemy import func, or_, text from sqlalchemy import func, text
import grant.utils.admin as admin import grant.utils.admin as admin
import grant.utils.auth as auth import grant.utils.auth as auth
@ -25,7 +25,7 @@ from grant.proposal.models import (
admin_proposal_contributions_schema, admin_proposal_contributions_schema,
) )
from grant.rfp.models import RFP, admin_rfp_schema, admin_rfps_schema from grant.rfp.models import RFP, admin_rfp_schema, admin_rfps_schema
from grant.user.models import User, UserSettings, admin_users_schema, admin_user_schema from grant.user.models import User, admin_users_schema, admin_user_schema
from grant.utils import pagination from grant.utils import pagination
from grant.utils.enums import ( from grant.utils.enums import (
ProposalStatus, ProposalStatus,
@ -390,6 +390,22 @@ def approve_proposal_kyc(id):
return proposal_schema.dump(proposal) return proposal_schema.dump(proposal)
@blueprint.route('/proposals/<id>/adjust-funder', methods=['PUT'])
@body({
"fundedByZomg": fields.Bool(required=True),
})
@admin.admin_auth_required
def adjust_funder(id, funded_by_zomg):
proposal = Proposal.query.get(id)
if not proposal:
return {"message": "No proposal found."}, 404
proposal.funded_by_zomg = funded_by_zomg
db.session.add(proposal)
db.session.commit()
return proposal_schema.dump(proposal)
@blueprint.route('/proposals/<id>/accept', methods=['PUT']) @blueprint.route('/proposals/<id>/accept', methods=['PUT'])
@body({ @body({
"isAccepted": fields.Bool(required=True), "isAccepted": fields.Bool(required=True),

View File

@ -1,8 +1,8 @@
import datetime import datetime
import json import json
from typing import Optional
from decimal import Decimal, ROUND_DOWN from decimal import Decimal, ROUND_DOWN
from functools import reduce from functools import reduce
from typing import Optional
from marshmallow import post_dump from marshmallow import post_dump
from sqlalchemy import func, or_, select, ForeignKey from sqlalchemy import func, or_, select, ForeignKey
@ -10,15 +10,14 @@ from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import column_property from sqlalchemy.orm import column_property
from grant.comment.models import Comment from grant.comment.models import Comment
from grant.milestone.models import Milestone
from grant.email.send import send_email from grant.email.send import send_email
from grant.extensions import ma, db from grant.extensions import ma, db
from grant.milestone.models import Milestone
from grant.settings import PROPOSAL_STAKING_AMOUNT, PROPOSAL_TARGET_MAX from grant.settings import PROPOSAL_STAKING_AMOUNT, PROPOSAL_TARGET_MAX
from grant.task.jobs import ContributionExpired from grant.task.jobs import ContributionExpired
from grant.utils.enums import ( from grant.utils.enums import (
ProposalStatus, ProposalStatus,
ProposalStage, ProposalStage,
Category,
ContributionStatus, ContributionStatus,
ProposalArbiterStatus, ProposalArbiterStatus,
MilestoneStage, MilestoneStage,
@ -332,7 +331,8 @@ class ProposalRevision(db.Model):
if old_proposal.title != new_proposal.title: if old_proposal.title != new_proposal.title:
proposal_changes.append({"type": ProposalChange.PROPOSAL_EDIT_TITLE}) proposal_changes.append({"type": ProposalChange.PROPOSAL_EDIT_TITLE})
milestone_changes = ProposalRevision.calculate_milestone_changes(old_proposal.milestones, new_proposal.milestones) milestone_changes = ProposalRevision.calculate_milestone_changes(old_proposal.milestones,
new_proposal.milestones)
return proposal_changes + milestone_changes return proposal_changes + milestone_changes
@ -392,6 +392,7 @@ class Proposal(db.Model):
date_published = db.Column(db.DateTime) date_published = db.Column(db.DateTime)
reject_reason = db.Column(db.String()) reject_reason = db.Column(db.String())
kyc_approved = db.Column(db.Boolean(), nullable=True, default=False) kyc_approved = db.Column(db.Boolean(), nullable=True, default=False)
funded_by_zomg = db.Column(db.Boolean(), nullable=True, default=False)
accepted_with_funding = db.Column(db.Boolean(), nullable=True) accepted_with_funding = db.Column(db.Boolean(), nullable=True)
changes_requested_discussion = db.Column(db.Boolean(), nullable=True) changes_requested_discussion = db.Column(db.Boolean(), nullable=True)
@ -422,21 +423,23 @@ class Proposal(db.Model):
) )
followers_count = column_property( followers_count = column_property(
select([func.count(proposal_follower.c.proposal_id)]) select([func.count(proposal_follower.c.proposal_id)])
.where(proposal_follower.c.proposal_id == id) .where(proposal_follower.c.proposal_id == id)
.correlate_except(proposal_follower) .correlate_except(proposal_follower)
) )
likes = db.relationship( likes = db.relationship(
"User", secondary=proposal_liker, back_populates="liked_proposals" "User", secondary=proposal_liker, back_populates="liked_proposals"
) )
likes_count = column_property( likes_count = column_property(
select([func.count(proposal_liker.c.proposal_id)]) select([func.count(proposal_liker.c.proposal_id)])
.where(proposal_liker.c.proposal_id == id) .where(proposal_liker.c.proposal_id == id)
.correlate_except(proposal_liker) .correlate_except(proposal_liker)
) )
live_draft_parent_id = db.Column(db.Integer, ForeignKey('proposal.id')) live_draft_parent_id = db.Column(db.Integer, ForeignKey('proposal.id'))
live_draft = db.relationship("Proposal", uselist=False, backref=db.backref('live_draft_parent', remote_side=[id], uselist=False)) live_draft = db.relationship("Proposal", uselist=False,
backref=db.backref('live_draft_parent', remote_side=[id], uselist=False))
revisions = db.relationship(ProposalRevision, foreign_keys=[ProposalRevision.proposal_id], lazy=True, cascade="all, delete-orphan") revisions = db.relationship(ProposalRevision, foreign_keys=[ProposalRevision.proposal_id], lazy=True,
cascade="all, delete-orphan")
def __init__( def __init__(
self, self,
@ -527,7 +530,7 @@ class Proposal(db.Model):
# Validate payout address # Validate payout address
if not is_z_address_valid(self.payout_address): if not is_z_address_valid(self.payout_address):
raise ValidationException("Payout address is not a valid z address") raise ValidationException("Payout address is not a valid z address")
# Validate tip jar address # Validate tip jar address
if self.tip_jar_address and not is_z_address_valid(self.tip_jar_address): if self.tip_jar_address and not is_z_address_valid(self.tip_jar_address):
raise ValidationException("Tip address is not a valid z address") raise ValidationException("Tip address is not a valid z address")
@ -535,7 +538,6 @@ class Proposal(db.Model):
# Then run through regular validation # Then run through regular validation
Proposal.simple_validate(vars(self)) Proposal.simple_validate(vars(self))
def validate_milestone_days(self): def validate_milestone_days(self):
for milestone in self.milestones: for milestone in self.milestones:
if milestone.immediate_payout: if milestone.immediate_payout:
@ -612,11 +614,11 @@ class Proposal(db.Model):
self.rfp_opt_in = opt_in self.rfp_opt_in = opt_in
def create_contribution( def create_contribution(
self, self,
amount, amount,
user_id: int = None, user_id: int = None,
staking: bool = False, staking: bool = False,
private: bool = True, private: bool = True,
): ):
contribution = ProposalContribution( contribution = ProposalContribution(
proposal_id=self.id, proposal_id=self.id,
@ -923,8 +925,8 @@ class Proposal(db.Model):
return False return False
res = ( res = (
db.session.query(proposal_follower) db.session.query(proposal_follower)
.filter_by(user_id=authed.id, proposal_id=self.id) .filter_by(user_id=authed.id, proposal_id=self.id)
.count() .count()
) )
if res: if res:
return True return True
@ -939,8 +941,8 @@ class Proposal(db.Model):
return False return False
res = ( res = (
db.session.query(proposal_liker) db.session.query(proposal_liker)
.filter_by(user_id=authed.id, proposal_id=self.id) .filter_by(user_id=authed.id, proposal_id=self.id)
.count() .count()
) )
if res: if res:
return True return True
@ -1099,7 +1101,8 @@ class ProposalSchema(ma.Schema):
"changes_requested_discussion", "changes_requested_discussion",
"changes_requested_discussion_reason", "changes_requested_discussion_reason",
"live_draft_id", "live_draft_id",
"kyc_approved" "kyc_approved",
"funded_by_zomg"
) )
date_created = ma.Method("get_date_created") date_created = ma.Method("get_date_created")
@ -1109,6 +1112,7 @@ class ProposalSchema(ma.Schema):
is_version_two = ma.Method("get_is_version_two") is_version_two = ma.Method("get_is_version_two")
tip_jar_view_key = ma.Method("get_tip_jar_view_key") tip_jar_view_key = ma.Method("get_tip_jar_view_key")
live_draft_id = ma.Method("get_live_draft_id") live_draft_id = ma.Method("get_live_draft_id")
funded_by_zomg = ma.Method("get_funded_by_zomg")
updates = ma.Nested("ProposalUpdateSchema", many=True) updates = ma.Nested("ProposalUpdateSchema", many=True)
team = ma.Nested("UserSchema", many=True) team = ma.Nested("UserSchema", many=True)
@ -1118,6 +1122,14 @@ class ProposalSchema(ma.Schema):
rfp = ma.Nested("RFPSchema", exclude=["accepted_proposals"]) rfp = ma.Nested("RFPSchema", exclude=["accepted_proposals"])
arbiter = ma.Nested("ProposalArbiterSchema", exclude=["proposal"]) arbiter = ma.Nested("ProposalArbiterSchema", exclude=["proposal"])
def get_funded_by_zomg(self, obj):
if obj.funded_by_zomg is None:
return False
elif obj.funded_by_zomg is False:
return False
else:
return True
def get_proposal_id(self, obj): def get_proposal_id(self, obj):
return obj.id return obj.id

View File

@ -0,0 +1,28 @@
"""empty message
Revision ID: 91b16dc2fd74
Revises: d03c91f3038d
Create Date: 2021-02-01 17:00:23.721765
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '91b16dc2fd74'
down_revision = 'd03c91f3038d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('proposal', sa.Column('funded_by_zomg', sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('proposal', 'funded_by_zomg')
# ### end Alembic commands ###

View File

@ -86,11 +86,11 @@ export const STAGE_UI: { [key in PROPOSAL_FILTERS]: StageUI } = {
color: '#8e44ad', color: '#8e44ad',
}, },
ACCEPTED_WITH_FUNDING: { ACCEPTED_WITH_FUNDING: {
label: 'Funded by ZF', label: 'Funded',
color: '#8e44ad', color: '#8e44ad',
}, },
ACCEPTED_WITHOUT_FUNDING: { ACCEPTED_WITHOUT_FUNDING: {
label: 'Not Funded by ZF', label: 'Not Funded',
color: '#8e44ad', color: '#8e44ad',
}, },
WIP: { WIP: {

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { UserProposal, STATUS } from 'types'; import { STATUS, UserProposal } from 'types';
import './ProfileProposal.less'; import './ProfileProposal.less';
import UserRow from 'components/UserRow'; import UserRow from 'components/UserRow';
import UnitDisplay from 'components/UnitDisplay'; import UnitDisplay from 'components/UnitDisplay';
@ -23,7 +23,8 @@ export default class Profile extends React.Component<OwnProps> {
isVersionTwo, isVersionTwo,
acceptedWithFunding, acceptedWithFunding,
status, status,
changesRequestedDiscussionReason changesRequestedDiscussionReason,
fundedByZomg,
} = this.props.proposal; } = this.props.proposal;
// pulled from `variables.less` // pulled from `variables.less`
@ -31,18 +32,24 @@ export default class Profile extends React.Component<OwnProps> {
const secondaryColor = '#2D2A26'; const secondaryColor = '#2D2A26';
const isOpenForDiscussion = status === STATUS.DISCUSSION; const isOpenForDiscussion = status === STATUS.DISCUSSION;
const discussionColor = changesRequestedDiscussionReason ? 'red' : infoColor const discussionColor = changesRequestedDiscussionReason ? 'red' : infoColor;
const discussionTag = changesRequestedDiscussionReason ? 'Changes Requested' : 'Open for Public Review' const discussionTag = changesRequestedDiscussionReason
? 'Changes Requested'
: 'Open for Public Review';
let tagColor = infoColor let tagColor = infoColor;
let tagMessage = 'Open for Contributions' let tagMessage = 'Open for Contributions';
if (acceptedWithFunding) { if (acceptedWithFunding) {
tagColor = secondaryColor tagColor = secondaryColor;
tagMessage = 'Funded by ZF' if (!fundedByZomg) {
tagMessage = 'Funded by ZF';
} else {
tagMessage = 'Funded by ZOMG';
}
} else if (isOpenForDiscussion) { } else if (isOpenForDiscussion) {
tagColor = discussionColor tagColor = discussionColor;
tagMessage = discussionTag tagMessage = discussionTag;
} }
return ( return (

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import { Icon, Popover, Tooltip, Alert } from 'antd'; import { Alert, Icon, Popover, Tooltip } from 'antd';
import { Proposal, STATUS } from 'types'; import { Proposal, STATUS } from 'types';
import classnames from 'classnames'; import classnames from 'classnames';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -12,6 +12,8 @@ import Loader from 'components/Loader';
import { PROPOSAL_STAGE } from 'api/constants'; import { PROPOSAL_STAGE } from 'api/constants';
import { formatUsd } from 'utils/formatters'; import { formatUsd } from 'utils/formatters';
import ZFGrantsLogo from 'static/images/logo-name-light.svg'; import ZFGrantsLogo from 'static/images/logo-name-light.svg';
import ZomgLogo from 'static/images/zomg-logo.png';
import './style.less'; import './style.less';
interface OwnProps { interface OwnProps {
@ -134,7 +136,11 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
isAcceptedWithFunding && ( isAcceptedWithFunding && (
<div className="ProposalCampaignBlock-with-funding"> <div className="ProposalCampaignBlock-with-funding">
Funded through &nbsp; Funded through &nbsp;
<ZFGrantsLogo style={{ height: '1.5rem' }} /> {proposal.fundedByZomg ? (
<img src={ZomgLogo} alt={'Zomg logo'} style={{ height: '1.5rem' }} />
) : (
<ZFGrantsLogo style={{ height: '1.5rem' }} />
)}
</div> </div>
)} )}

View File

@ -29,6 +29,7 @@ export class ProposalCard extends React.Component<Proposal> {
percentFunded, percentFunded,
acceptedWithFunding, acceptedWithFunding,
status, status,
fundedByZomg,
} = this.props; } = this.props;
// pulled from `variables.less` // pulled from `variables.less`
@ -46,7 +47,11 @@ export class ProposalCard extends React.Component<Proposal> {
if (isVersionTwo && status === STATUS.LIVE) { if (isVersionTwo && status === STATUS.LIVE) {
if (acceptedWithFunding) { if (acceptedWithFunding) {
tagColor = secondaryColor; tagColor = secondaryColor;
tagMessage = 'Funded by ZF'; if (!fundedByZomg) {
tagMessage = 'Funded by ZF';
} else {
tagMessage = 'Funded by ZOMG';
}
} else { } else {
tagColor = infoColor; tagColor = infoColor;
tagMessage = 'Not Funded'; tagMessage = 'Not Funded';

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1300.8 419.2" style="enable-background:new 0 0 1300.8 419.2;" xml:space="preserve">
<style type="text/css">
.st0{display:none;}
.st1{display:inline;fill:#100400;}
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
.st3{fill-rule:evenodd;clip-rule:evenodd;fill:#0F7000;}
.st4{fill:#FFFFFF;}
.st5{fill:#F8BB14;}
.st6{fill:none;stroke:#FFFFFF;stroke-width:24.24;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;}
.st7{font-family:'Roboto-Medium';}
.st8{font-size:304.0703px;}
</style>
<g id="Layer_2" class="st0">
<rect x="-102.5" y="-82" class="st1" width="1515.7" height="567.6"/>
</g>
<g id="Layer_1" xmlns:serif="http://www.serif.com/">
<g>
<g transform="matrix(0.347046,-4.1523e-17,-1.03807e-17,0.347046,307.53,12.3394)">
<g id="Color">
<g>
<g id="Color1" transform="matrix(1.09544,1.31066e-16,3.27664e-17,1.09544,-953.204,-33.2755)" serif:id="Color">
<g transform="matrix(1.19303,-3.26522e-48,2.90837e-48,1.06265,-101.088,-115.302)">
<rect x="572.7" y="564.3" class="st2" width="37.4" height="265.4"/>
</g>
<g transform="matrix(1,-2.73691e-48,2.73691e-48,1,0,-21.0434)">
<path class="st3" d="M970,472.4c-7.7-25.6-24.4,55.7-167.7,79.1c-112.6,18.3-168.6,129.2-148.5,161.9
c13.1,21.4,111.2,52.7,194.7-8.3C931.7,644.3,977.7,498,970,472.4z M651.6,707c3,5.4,78.8-15.7,123.6-40.6
c52.8-29.3,108.9-79.9,104.4-85.7c-2.9-3.8-63.8,23.9-123.7,57.7C704.1,667.6,646.4,697.7,651.6,707z"/>
</g>
<g transform="matrix(1,-2.73691e-48,2.73691e-48,1,0,-21.0434)">
<path class="st3" d="M295.5,443.5C308,424.7,306.9,490,436.7,528c102,29.9,131.7,123.6,106.1,146.1
c-16.7,14.7-115.4,25.3-181.2-33.5C295.9,581.9,283,462.2,295.5,443.5z M544.7,672c-2.8,3.9-52.5-14.7-91.1-39
c-30-19-76.9-55.4-64.6-63.8c5.6-3.8,41.1,21,77.6,44.2C504.4,637.4,550.2,664.4,544.7,672z"/>
</g>
<g transform="matrix(0.927915,-1.11022e-16,-2.77556e-17,0.927915,854.005,46.6736)">
<g transform="matrix(1,0,0,1,-581.95,-44.2319)">
<path class="st4" d="M312.2,514.8c-131.2,0-238-106.8-238-238c0-131.2,106.8-238,238-238c131.2,0,238,106.8,238,238
C550.2,408,443.4,514.8,312.2,514.8z M312.2-8.3C155-8.3,27.1,119.6,27.1,276.8S155,561.9,312.2,561.9
S597.3,434,597.3,276.8S469.4-8.3,312.2-8.3z"/>
</g>
<path class="st5" d="M-269.8,7.8c-123.9,0-224.7,100.8-224.7,224.7c0,123.9,100.8,224.7,224.7,224.7S-45.1,356.4-45.1,232.5
C-45.1,108.6-145.9,7.8-269.8,7.8z M-168.1,143.7l-22,27.9l-98.6,135.7h120.6v57.6h-77.8v47.6h-5.7v0.2h-36.3v-0.2h-5.7
v-47.6h-77.8v-43.5l22-27.9l98.6-135.7h-120.6v-57.6h77.8V52.5h47.8v47.7h77.8V143.7z"/>
</g>
</g>
<g id="Hand" transform="matrix(1.23479,1.47739e-16,3.95e-17,1.32055,-1023.9,-346.117)">
<path class="st6" d="M139.9,880c0,0,93-1.5,138.8-9.1c48.9-8.1,105.2-39.7,154.8-39.4c49.5,0.3,87.5,32.1,142.6,41.4
c59,10,156.2,9.9,180.2,27.8c19.8,14.7,5.2,49.9-32.8,64c-26,9.7-73.4,19.4-110.9,19.1c-53.6-0.5-217.9-23.2-210.9-21.8
c7,1.4,168.5,39.2,252.9,30.2c84.8-9,192.9-67.4,256.1-84.4c40-10.7,92.5-19.5,123.1-17.3c38.6,2.8,64,22.1,47.7,32.5
c-57.7,36.8-263.6,156-393.9,188.1c-86.5,21.3-273,9.5-313,4.2c-60.9-8-98.3-24.4-139.4-53.2c-23.7-16.6-95.3-45.2-95.3-45.2
V880z"/>
</g>
<g transform="matrix(121.473,1.45339e-14,3.31689e-15,110.889,-21599.6,-106761)">
<text transform="matrix(2.598518e-02 0 0 2.598510e-02 181.6097 969.9001)" class="st4 st7 st8">zomg</text>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -81,6 +81,7 @@ export interface Proposal extends Omit<ProposalDraft, 'target' | 'invites'> {
liveDraftId: string | null; liveDraftId: string | null;
isTeamMember?: boolean; // FE derived isTeamMember?: boolean; // FE derived
isArbiter?: boolean; // FE derived isArbiter?: boolean; // FE derived
fundedByZomg: boolean;
} }
export interface TeamInviteWithProposal extends TeamInvite { export interface TeamInviteWithProposal extends TeamInvite {