Merge pull request #179 from grant-project/rfp-fields

RFP Fields (matching, bounty, dates)
This commit is contained in:
Daniel Ternyak 2019-02-15 12:05:59 -06:00 committed by GitHub
commit fe5632dc75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 339 additions and 29 deletions

View File

@ -91,6 +91,9 @@ class RFPDetail extends React.Component<Props> {
{renderDeetItem('created', formatDateSeconds(rfp.dateCreated))} {renderDeetItem('created', formatDateSeconds(rfp.dateCreated))}
{renderDeetItem('status', rfp.status)} {renderDeetItem('status', rfp.status)}
{renderDeetItem('category', rfp.category)} {renderDeetItem('category', rfp.category)}
{renderDeetItem('matching', String(rfp.matching))}
{renderDeetItem('bounty', `${rfp.bounty} ZEC`)}
{renderDeetItem('dateCloses', rfp.dateCloses && formatDateSeconds(rfp.dateCloses))}
</Card> </Card>
{/* PROPOSALS */} {/* PROPOSALS */}

View File

@ -1,4 +1,6 @@
.RFPForm { .RFPForm {
max-width: 880px;
&-content { &-content {
&-preview { &-preview {
font-size: 1rem; font-size: 1rem;
@ -13,6 +15,24 @@
} }
} }
&-bounty {
&-matching.ant-checkbox-wrapper {
margin-top: 0.4rem;
}
}
&-date {
padding-left: 1.5rem;
.ant-calendar-picker {
width: 100%;
}
.ant-form-explain {
margin-top: 0.4rem;
}
}
&-buttons { &-buttons {
.ant-btn { .ant-btn {
margin-right: 0.5rem; margin-right: 0.5rem;

View File

@ -1,7 +1,9 @@
import React from 'react'; import React from 'react';
import moment, { Moment } from 'moment';
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 { Form, Input, Select, Icon, Button, message, Spin } from 'antd'; import { Link } from 'react-router-dom';
import { Form, Input, Select, Icon, Button, message, Spin, Checkbox, Row, Col, DatePicker } from 'antd';
import Exception from 'ant-design-pro/lib/Exception'; import Exception from 'ant-design-pro/lib/Exception';
import { FormComponentProps } from 'antd/lib/form'; import { FormComponentProps } from 'antd/lib/form';
import { PROPOSAL_CATEGORY, RFP_STATUS, RFPArgs } from 'src/types'; import { PROPOSAL_CATEGORY, RFP_STATUS, RFPArgs } from 'src/types';
@ -34,7 +36,7 @@ class RFPForm extends React.Component<Props, State> {
render() { render() {
const { isShowingPreview } = this.state; const { isShowingPreview } = this.state;
const { getFieldDecorator, getFieldValue } = this.props.form; const { getFieldDecorator, getFieldValue, isFieldsTouched } = this.props.form;
let defaults: RFPArgs = { let defaults: RFPArgs = {
title: '', title: '',
@ -42,6 +44,9 @@ class RFPForm extends React.Component<Props, State> {
content: '', content: '',
category: '', category: '',
status: '', status: '',
matching: false,
bounty: undefined,
dateCloses: undefined,
}; };
const rfpId = this.getRFPId(); const rfpId = this.getRFPId();
if (rfpId) { if (rfpId) {
@ -57,11 +62,19 @@ class RFPForm extends React.Component<Props, State> {
content: rfp.content, content: rfp.content,
category: rfp.category, category: rfp.category,
status: rfp.status, status: rfp.status,
matching: rfp.matching,
bounty: rfp.bounty,
dateCloses: rfp.dateCloses || undefined,
}; };
} else { } else {
return <Exception type="404" desc="This RFP does not exist" />; return <Exception type="404" desc="This RFP does not exist" />;
} }
} }
const dateCloses = isFieldsTouched(['dateCloses'])
? getFieldValue('dateCloses')
: defaults.dateCloses && moment(defaults.dateCloses * 1000);
const forceClosed = dateCloses && dateCloses.isBefore(moment.now());
return ( return (
<Form className="RFPForm" layout="vertical" onSubmit={this.handleSubmit}> <Form className="RFPForm" layout="vertical" onSubmit={this.handleSubmit}>
@ -85,12 +98,15 @@ class RFPForm extends React.Component<Props, State> {
</Form.Item> </Form.Item>
{rfpId && ( {rfpId && (
<Form.Item label="Status"> <Form.Item
label="Status"
help={forceClosed && 'Status is forced to "Closed" when close date is in the past'}
>
{getFieldDecorator('status', { {getFieldDecorator('status', {
initialValue: defaults.status, initialValue: defaults.status,
rules: [{ required: true, message: 'Status is required' }], rules: [{ required: true, message: 'Status is required' }],
})( })(
<Select size="large" placeholder="Select a status"> <Select size="large" placeholder="Select a status" disabled={forceClosed}>
{typedKeys(RFP_STATUS).map(c => ( {typedKeys(RFP_STATUS).map(c => (
<Select.Option value={c} key={c}> <Select.Option value={c} key={c}>
{getStatusById(RFP_STATUSES, c).tagDisplay} {getStatusById(RFP_STATUSES, c).tagDisplay}
@ -164,13 +180,57 @@ class RFPForm extends React.Component<Props, State> {
)} )}
</Form.Item> </Form.Item>
<Row>
<Col sm={12} xs={12}>
<Form.Item className="RFPForm-bounty" label="Bounty">
{getFieldDecorator('bounty', {
initialValue: defaults.bounty,
})(
<Input
autoComplete="off"
name="bounty"
placeholder="100"
addonAfter="ZEC"
size="large"
/>,
)}
{getFieldDecorator('matching', {
initialValue: defaults.matching,
})(
<Checkbox
className="RFPForm-bounty-matching"
name="matching"
defaultChecked={defaults.matching}
>
Match community contributions for approved proposals
</Checkbox>,
)}
</Form.Item>
</Col>
<Col sm={12} xs={24}>
<Form.Item
className="RFPForm-date"
label="Close date"
help="Date that proposals will stop being submittable by"
>
{getFieldDecorator('dateCloses', {
initialValue: defaults.dateCloses ? moment(defaults.dateCloses * 1000) : undefined,
})(
<DatePicker size="large" />
)}
</Form.Item>
</Col>
</Row>
<div className="RFPForm-buttons"> <div className="RFPForm-buttons">
<Button type="primary" htmlType="submit" size="large"> <Button type="primary" htmlType="submit" size="large">
Submit Submit
</Button> </Button>
<Button type="ghost" size="large"> <Link to="/rfps">
Cancel <Button type="ghost" size="large">
</Button> Cancel
</Button>
</Link>
</div> </div>
</Form> </Form>
); );
@ -189,10 +249,14 @@ class RFPForm extends React.Component<Props, State> {
private handleSubmit = (ev: React.FormEvent<HTMLFormElement>) => { private handleSubmit = (ev: React.FormEvent<HTMLFormElement>) => {
ev.preventDefault(); ev.preventDefault();
this.props.form.validateFieldsAndScroll(async (err: any, values: any) => { this.props.form.validateFieldsAndScroll(async (err: any, rawValues: any) => {
if (err) return; if (err) return;
const rfpId = this.getRFPId(); const rfpId = this.getRFPId();
const values = {
...rawValues,
dateCloses: rawValues.dateCloses && rawValues.dateCloses.unix(),
};
let msg; let msg;
if (rfpId) { if (rfpId) {
await store.editRFP(rfpId, values); await store.editRFP(rfpId, values);

View File

@ -36,19 +36,27 @@ export enum RFP_STATUS {
export interface RFP { export interface RFP {
id: number; id: number;
dateCreated: number; dateCreated: number;
dateOpened: number | null;
dateClosed: number | null;
title: string; title: string;
brief: string; brief: string;
content: string; content: string;
category: string; category: string;
status: string; status: string;
proposals: Proposal[]; proposals: Proposal[];
matching: boolean;
bounty: string | null;
dateCloses: number | null;
} }
export interface RFPArgs { export interface RFPArgs {
title: string; title: string;
brief: string; brief: string;
content: string; content: string;
category: string; category: string;
status?: string; matching: boolean;
dateCloses: number | null | undefined;
bounty: string | null | undefined;
status: string;
} }
// NOTE: sync with backend/grant/utils/enums.py ProposalArbiterStatus // NOTE: sync with backend/grant/utils/enums.py ProposalArbiterStatus
export enum PROPOSAL_ARBITER_STATUS { export enum PROPOSAL_ARBITER_STATUS {

View File

@ -2,6 +2,7 @@ from functools import reduce
from flask import Blueprint, request from flask import Blueprint, request
from flask_yoloapi import endpoint, parameter from flask_yoloapi import endpoint, parameter
from decimal import Decimal from decimal import Decimal
from datetime import datetime
from grant.comment.models import Comment, user_comments_schema from grant.comment.models import Comment, user_comments_schema
from grant.email.send import generate_email, send_email from grant.email.send import generate_email, send_email
from grant.extensions import db from grant.extensions import db
@ -19,7 +20,14 @@ from grant.user.models import User, admin_users_schema, admin_user_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.utils.admin import admin_auth_required, admin_is_authed, admin_login, admin_logout from grant.utils.admin import admin_auth_required, admin_is_authed, admin_login, admin_logout
from grant.utils.misc import make_url from grant.utils.misc import make_url
from grant.utils.enums import ProposalStatus, ProposalStage, ContributionStatus, ProposalArbiterStatus, MilestoneStage from grant.utils.enums import (
ProposalStatus,
ProposalStage,
ContributionStatus,
ProposalArbiterStatus,
MilestoneStage,
RFPStatus,
)
from grant.utils import pagination from grant.utils import pagination
from grant.settings import EXPLORER_URL from grant.settings import EXPLORER_URL
from sqlalchemy import func, or_ from sqlalchemy import func, or_
@ -332,14 +340,15 @@ def get_rfps():
parameter('brief', type=str), parameter('brief', type=str),
parameter('content', type=str), parameter('content', type=str),
parameter('category', type=str), parameter('category', type=str),
parameter('bounty', type=str),
parameter('matching', type=bool, default=False),
parameter('dateCloses', type=int),
) )
@admin_auth_required @admin_auth_required
def create_rfp(title, brief, content, category): def create_rfp(date_closes, **kwargs):
rfp = RFP( rfp = RFP(
title=title, **kwargs,
brief=brief, date_closes=datetime.fromtimestamp(date_closes) if date_closes else None,
content=content,
category=category,
) )
db.session.add(rfp) db.session.add(rfp)
db.session.commit() db.session.commit()
@ -363,19 +372,33 @@ def get_rfp(rfp_id):
parameter('brief', type=str), parameter('brief', type=str),
parameter('content', type=str), parameter('content', type=str),
parameter('category', type=str), parameter('category', type=str),
parameter('bounty', type=str),
parameter('matching', type=bool, default=False),
parameter('dateCloses', type=int),
parameter('status', type=str), parameter('status', type=str),
) )
@admin_auth_required @admin_auth_required
def update_rfp(rfp_id, title, brief, content, category, status): def update_rfp(rfp_id, title, brief, content, category, bounty, matching, date_closes, status):
rfp = RFP.query.filter(RFP.id == rfp_id).first() rfp = RFP.query.filter(RFP.id == rfp_id).first()
if not rfp: if not rfp:
return {"message": "No RFP matching that id"}, 404 return {"message": "No RFP matching that id"}, 404
# Update fields
rfp.title = title rfp.title = title
rfp.brief = brief rfp.brief = brief
rfp.content = content rfp.content = content
rfp.category = category rfp.category = category
rfp.status = status rfp.bounty = bounty
rfp.matching = matching
rfp.date_closes = datetime.fromtimestamp(date_closes) if date_closes else None
# Update timestamps if status changed
if rfp.status != status:
if status == RFPStatus.LIVE and not rfp.date_opened:
rfp.date_opened = datetime.now()
if status == RFPStatus.CLOSED:
rfp.date_closed = datetime.now()
rfp.status = status
db.session.add(rfp) db.session.add(rfp)
db.session.commit() db.session.commit()

View File

@ -164,9 +164,9 @@ def make_proposal_draft(rfp_id):
rfp = RFP.query.filter_by(id=rfp_id).first() rfp = RFP.query.filter_by(id=rfp_id).first()
if not rfp: if not rfp:
return {"message": "The request this proposal was made for doesnt exist"}, 400 return {"message": "The request this proposal was made for doesnt exist"}, 400
proposal.title = rfp.title
proposal.brief = rfp.brief
proposal.category = rfp.category proposal.category = rfp.category
if rfp.matching:
proposal.contribution_matching = 1.0
rfp.proposals.append(proposal) rfp.proposals.append(proposal)
db.session.add(rfp) db.session.add(rfp)

View File

@ -1,4 +1,4 @@
import datetime from datetime import datetime
from grant.extensions import ma, db from grant.extensions import ma, db
from grant.utils.enums import RFPStatus from grant.utils.enums import RFPStatus
from grant.utils.misc import dt_to_unix from grant.utils.misc import dt_to_unix
@ -15,6 +15,11 @@ class RFP(db.Model):
content = db.Column(db.Text, nullable=False) content = db.Column(db.Text, nullable=False)
category = db.Column(db.String(255), nullable=False) category = db.Column(db.String(255), nullable=False)
status = db.Column(db.String(255), nullable=False) status = db.Column(db.String(255), nullable=False)
matching = db.Column(db.Boolean, default=False, nullable=False)
bounty = db.Column(db.String(255), nullable=True)
date_closes = db.Column(db.DateTime, nullable=True)
date_opened = db.Column(db.DateTime, nullable=True)
date_closed = db.Column(db.DateTime, nullable=True)
# Relationships # Relationships
proposals = db.relationship( proposals = db.relationship(
@ -36,13 +41,19 @@ class RFP(db.Model):
brief: str, brief: str,
content: str, content: str,
category: str, category: str,
bounty: str,
date_closes: datetime,
matching: bool = False,
status: str = RFPStatus.DRAFT, status: str = RFPStatus.DRAFT,
): ):
self.date_created = datetime.datetime.now() self.date_created = datetime.now()
self.title = title self.title = title
self.brief = brief self.brief = brief
self.content = content self.content = content
self.category = category self.category = category
self.bounty = bounty
self.date_closes = date_closes
self.matching = matching
self.status = status self.status = status
@ -57,15 +68,35 @@ class RFPSchema(ma.Schema):
"content", "content",
"category", "category",
"status", "status",
"matching",
"bounty",
"date_created", "date_created",
"date_closes",
"date_opened",
"date_closed",
"accepted_proposals", "accepted_proposals",
) )
date_created = ma.Method("get_date_created") status = ma.Method("get_status")
date_closes = ma.Method("get_date_closes")
date_opened = ma.Method("get_date_opened")
date_closed = ma.Method("get_date_closed")
accepted_proposals = ma.Nested("ProposalSchema", many=True, exclude=["rfp"]) accepted_proposals = ma.Nested("ProposalSchema", many=True, exclude=["rfp"])
def get_date_created(self, obj): def get_status(self, obj):
return dt_to_unix(obj.date_created) # Force it into closed state if date_closes is in the past
if obj.date_closes and obj.date_closes <= datetime.today():
return RFPStatus.CLOSED
return obj.status
def get_date_closes(self, obj):
return dt_to_unix(obj.date_closes) if obj.date_closes else None
def get_date_opened(self, obj):
return dt_to_unix(obj.date_opened) if obj.date_opened else None
def get_date_closed(self, obj):
return dt_to_unix(obj.date_closed) if obj.date_closed else None
rfp_schema = RFPSchema() rfp_schema = RFPSchema()
rfps_schema = RFPSchema(many=True) rfps_schema = RFPSchema(many=True)
@ -82,15 +113,39 @@ class AdminRFPSchema(ma.Schema):
"content", "content",
"category", "category",
"status", "status",
"matching",
"bounty",
"date_created", "date_created",
"date_closes",
"date_opened",
"date_closed",
"proposals", "proposals",
) )
status = ma.Method("get_status")
date_created = ma.Method("get_date_created") date_created = ma.Method("get_date_created")
date_closes = ma.Method("get_date_closes")
date_opened = ma.Method("get_date_opened")
date_closed = ma.Method("get_date_closed")
proposals = ma.Nested("ProposalSchema", many=True, exclude=["rfp"]) proposals = ma.Nested("ProposalSchema", many=True, exclude=["rfp"])
def get_status(self, obj):
# Force it into closed state if date_closes is in the past
if obj.date_closes and obj.date_closes <= datetime.today():
return RFPStatus.CLOSED
return obj.status
def get_date_created(self, obj): def get_date_created(self, obj):
return dt_to_unix(obj.date_created) return dt_to_unix(obj.date_created)
def get_date_closes(self, obj):
return dt_to_unix(obj.date_closes) if obj.date_closes else None
def get_date_opened(self, obj):
return dt_to_unix(obj.date_opened) if obj.date_opened else None
def get_date_closed(self, obj):
return dt_to_unix(obj.date_closes) if obj.date_closes else None
admin_rfp_schema = AdminRFPSchema() admin_rfp_schema = AdminRFPSchema()
admin_rfps_schema = AdminRFPSchema(many=True) admin_rfps_schema = AdminRFPSchema(many=True)

View File

@ -0,0 +1,51 @@
"""Adds RFP bounty, matching, and date fields
Revision ID: d39bb526eef4
Revises: 3793d9a71e27
Create Date: 2019-02-07 15:09:11.548655
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import expression
from datetime import datetime, timedelta
# revision identifiers, used by Alembic.
revision = 'd39bb526eef4'
down_revision = '3793d9a71e27'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('rfp', sa.Column('bounty', sa.String(length=255), nullable=True))
op.add_column('rfp', sa.Column('date_closed', sa.DateTime(), nullable=True))
op.add_column('rfp', sa.Column('date_closes', sa.DateTime(), nullable=True))
op.add_column('rfp', sa.Column('date_opened', sa.DateTime(), nullable=True))
op.add_column('rfp', sa.Column('matching', sa.Boolean(), nullable=False, server_default=expression.false()))
# ### end Alembic commands ###
# Set columns for times based on status.
connection = op.get_bind()
connection.execute("UPDATE rfp SET date_opened = now() - INTERVAL '1 DAY' WHERE status = 'LIVE' OR status = 'CLOSED'")
connection.execute("UPDATE rfp SET date_closed = now() WHERE status = 'CLOSED'")
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('rfp', 'matching')
op.drop_column('rfp', 'date_opened')
op.drop_column('rfp', 'date_closes')
op.drop_column('rfp', 'date_closed')
op.drop_column('rfp', 'bounty')
op.create_table('rfp_proposal',
sa.Column('rfp_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('proposal_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['proposal_id'], ['proposal.id'], name='rfp_proposal_proposal_id_fkey'),
sa.ForeignKeyConstraint(['rfp_id'], ['rfp.id'], name='rfp_proposal_rfp_id_fkey'),
sa.UniqueConstraint('proposal_id', name='rfp_proposal_proposal_id_key')
)
# ### end Alembic commands ###

View File

@ -32,9 +32,23 @@
&-content { &-content {
font-size: 1.15rem; font-size: 1.15rem;
margin-bottom: 2rem;
}
&-rules {
padding-bottom: 2rem; padding-bottom: 2rem;
margin-bottom: 2rem; margin-bottom: 2rem;
border-bottom: 1px solid rgba(#000, 0.08); border-bottom: 1px solid rgba(#000, 0.08);
ul {
margin: 0;
padding: 0 0 0 1rem;
li {
font-size: 0.9rem;
line-height: 2rem;
}
}
} }
&-proposals { &-proposals {

View File

@ -11,6 +11,7 @@ import { AppState } from 'store/reducers';
import Loader from 'components/Loader'; import Loader from 'components/Loader';
import Markdown from 'components/Markdown'; import Markdown from 'components/Markdown';
import ProposalCard from 'components/Proposals/ProposalCard'; import ProposalCard from 'components/Proposals/ProposalCard';
import UnitDisplay from 'components/UnitDisplay';
import './index.less'; import './index.less';
interface OwnProps { interface OwnProps {
@ -53,11 +54,34 @@ class RFPDetail extends React.Component<Props> {
<Icon type="arrow-left" /> Back to Requests <Icon type="arrow-left" /> Back to Requests
</Link> </Link>
<div className="RFPDetail-top-date"> <div className="RFPDetail-top-date">
Opened {moment(rfp.dateCreated * 1000).format('LL')} Opened {moment(rfp.dateOpened * 1000).format('LL')}
</div> </div>
</div> </div>
<h1 className="RFPDetail-title">{rfp.title}</h1> <h1 className="RFPDetail-title">{rfp.title}</h1>
<Markdown className="RFPDetail-content" source={rfp.content} /> <Markdown className="RFPDetail-content" source={rfp.content} />
<div className="RFPDetail-rules">
<ul>
{rfp.bounty && (
<li>
Accepted proposals will be funded up to{' '}
<strong>
<UnitDisplay value={rfp.bounty} symbol="ZEC" />
</strong>
</li>
)}
{rfp.matching && (
<li>
Contributions will have their <strong>funding matched</strong> by the
Zcash Foundation
</li>
)}
{rfp.dateCloses && (
<li>
Proposal submissions end {moment(rfp.dateCloses * 1000).format('LL')}
</li>
)}
</ul>
</div>
{!!rfp.acceptedProposals.length && ( {!!rfp.acceptedProposals.length && (
<div className="RFPDetail-proposals"> <div className="RFPDetail-proposals">

View File

@ -24,6 +24,11 @@
font-size: 1.4rem; font-size: 1.4rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
color: @primary-color; color: @primary-color;
.ant-tag {
vertical-align: top;
margin: 0.5rem 0 0 0.5rem;
}
} }
&-brief { &-brief {

View File

@ -1,7 +1,9 @@
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import classnames from 'classnames'; import classnames from 'classnames';
import { Tag } from 'antd';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import UnitDisplay from 'components/UnitDisplay';
import { RFP } from 'types'; import { RFP } from 'types';
import './RFPItem.less'; import './RFPItem.less';
@ -13,19 +15,52 @@ interface Props {
export default class RFPItem extends React.Component<Props> { export default class RFPItem extends React.Component<Props> {
render() { render() {
const { rfp, isSmall } = this.props; const { rfp, isSmall } = this.props;
const { id, title, brief, acceptedProposals, dateCreated } = rfp; const {
id,
title,
brief,
acceptedProposals,
dateOpened,
dateCloses,
dateClosed,
bounty,
matching,
} = rfp;
const closeDate = dateCloses || dateClosed;
const tags = [];
if (!isSmall) {
if (bounty) {
tags.push(
<Tag key="bounty" color="#CF8A00">
<UnitDisplay value={bounty} symbol="ZEC" /> bounty
</Tag>,
);
}
if (matching) {
tags.push(
<Tag key="matching" color="#1890ff">
x2 matching
</Tag>,
);
}
}
return ( return (
<Link <Link
className={classnames('RFPItem', isSmall && 'is-small')} className={classnames('RFPItem', isSmall && 'is-small')}
to={`/requests/${id}`} to={`/requests/${id}`}
> >
<h3 className="RFPItem-title">{title}</h3> <h3 className="RFPItem-title">
{title}
{tags}
</h3>
<p className="RFPItem-brief">{brief}</p> <p className="RFPItem-brief">{brief}</p>
<div className="RFPItem-details"> <div className="RFPItem-details">
<div className="RFPItem-details-detail"> <div className="RFPItem-details-detail">
{moment(dateCreated * 1000).format('LL')} {moment(dateOpened * 1000).format('LL')}
{closeDate && <> {moment(closeDate * 1000).format('LL')}</>}
</div> </div>
<div className="RFPItem-details-detail"> <div className="RFPItem-details-detail">
{acceptedProposals.length} proposals approved {acceptedProposals.length} proposals approved

View File

@ -101,6 +101,9 @@ export function formatProposalFromGet(p: any): Proposal {
} }
export function formatRFPFromGet(rfp: RFP): RFP { export function formatRFPFromGet(rfp: RFP): RFP {
if (rfp.bounty) {
rfp.bounty = toZat(rfp.bounty as any);
}
rfp.acceptedProposals = rfp.acceptedProposals.map(formatProposalFromGet); rfp.acceptedProposals = rfp.acceptedProposals.map(formatProposalFromGet);
return rfp; return rfp;
} }

View File

@ -1,13 +1,18 @@
import { Proposal } from './proposal'; import { Proposal } from './proposal';
import { PROPOSAL_CATEGORY, RFP_STATUS } from 'api/constants'; import { PROPOSAL_CATEGORY, RFP_STATUS } from 'api/constants';
import { Zat } from 'utils/units';
export interface RFP { export interface RFP {
id: number; id: number;
dateCreated: number;
title: string; title: string;
brief: string; brief: string;
content: string; content: string;
category: PROPOSAL_CATEGORY; category: PROPOSAL_CATEGORY;
status: RFP_STATUS; status: RFP_STATUS;
acceptedProposals: Proposal[]; acceptedProposals: Proposal[];
bounty: Zat | null;
matching: boolean;
dateOpened: number;
dateClosed?: number;
dateCloses?: number;
} }