From b03a9d3caf3a08ce18be788ee1b7aef990d8525f Mon Sep 17 00:00:00 2001 From: Will O'Beirne Date: Thu, 7 Feb 2019 15:35:33 -0500 Subject: [PATCH 1/8] Update RFP model with new fields, api endpoint for editing them. --- backend/grant/admin/views.py | 33 ++++++++---- backend/grant/rfp/models.py | 27 +++++++++- backend/migrations/versions/d39bb526eef4_.py | 53 ++++++++++++++++++++ 3 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 backend/migrations/versions/d39bb526eef4_.py diff --git a/backend/grant/admin/views.py b/backend/grant/admin/views.py index c5fab00d..19a88378 100644 --- a/backend/grant/admin/views.py +++ b/backend/grant/admin/views.py @@ -2,6 +2,7 @@ from flask import Blueprint, request from flask import Blueprint, request from flask_yoloapi import endpoint, parameter from decimal import Decimal +from datetime import datetime from grant.comment.models import Comment, user_comments_schema from grant.email.send import generate_email, send_email from grant.extensions import db @@ -17,7 +18,7 @@ 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.utils.admin import admin_auth_required, admin_is_authed, admin_login, admin_logout from grant.utils.misc import make_url -from grant.utils.enums import ProposalStatus, ContributionStatus +from grant.utils.enums import ProposalStatus, ContributionStatus, RFPStatus from grant.utils import pagination from sqlalchemy import func, or_ @@ -284,15 +285,13 @@ def get_rfps(): parameter('brief', type=str), parameter('content', type=str), parameter('category', type=str), + parameter('bounty', type=str), + parameter('matching', type=bool), + parameter('dateCloses', type=int), ) @admin_auth_required -def create_rfp(title, brief, content, category): - rfp = RFP( - title=title, - brief=brief, - content=content, - category=category, - ) +def create_rfp(**kwargs): + rfp = RFP(**kwargs) db.session.add(rfp) db.session.commit() return admin_rfp_schema.dump(rfp), 201 @@ -315,19 +314,33 @@ def get_rfp(rfp_id): parameter('brief', type=str), parameter('content', type=str), parameter('category', type=str), + parameter('bounty', type=str), + parameter('matching', type=bool), + parameter('dateCloses', type=int), parameter('status', type=str), ) @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() if not rfp: return {"message": "No RFP matching that id"}, 404 + # Update fields rfp.title = title rfp.brief = brief rfp.content = content rfp.category = category - rfp.status = status + rfp.bounty = bounty + rfp.matching = matching + rfp.date_closes = date_closes + + # Update timestamps if status changed + if rfp.status != status: + if status == RFPStatus.LIVE: + rfp.date_opened = datetime.now() + if status == RFPStatus.CLOSED: + rfp.date_closed = datetime.now() + rfp.status = status db.session.add(rfp) db.session.commit() diff --git a/backend/grant/rfp/models.py b/backend/grant/rfp/models.py index 82bd82c7..fec9d07e 100644 --- a/backend/grant/rfp/models.py +++ b/backend/grant/rfp/models.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime from grant.extensions import ma, db from grant.utils.enums import RFPStatus from grant.utils.misc import dt_to_unix @@ -15,6 +15,11 @@ class RFP(db.Model): content = db.Column(db.Text, nullable=False) category = 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 proposals = db.relationship( @@ -36,13 +41,19 @@ class RFP(db.Model): brief: str, content: str, category: str, + bounty: str, + date_closes: datetime, + matching: bool = False, status: str = RFPStatus.DRAFT, ): - self.date_created = datetime.datetime.now() + self.date_created = datetime.now() self.title = title self.brief = brief self.content = content self.category = category + self.bounty = bounty + self.date_closes = date_closes + self.matching = matching self.status = status @@ -87,10 +98,22 @@ class AdminRFPSchema(ma.Schema): ) 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"]) def get_date_created(self, obj): return dt_to_unix(obj.date_created) + + def get_date_closes(self, obj): + return dt_to_unix(obj.date_closes) + + def get_date_opened(self, obj): + return dt_to_unix(obj.date_opened) + + def get_date_closed(self, obj): + return dt_to_unix(obj.date_closes) admin_rfp_schema = AdminRFPSchema() admin_rfps_schema = AdminRFPSchema(many=True) diff --git a/backend/migrations/versions/d39bb526eef4_.py b/backend/migrations/versions/d39bb526eef4_.py new file mode 100644 index 00000000..f89f9444 --- /dev/null +++ b/backend/migrations/versions/d39bb526eef4_.py @@ -0,0 +1,53 @@ +"""Adds RFP bounty, matching, and date fields + +Revision ID: d39bb526eef4 +Revises: 310dca400b81 +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 + + +# revision identifiers, used by Alembic. +revision = 'd39bb526eef4' +down_revision = '310dca400b81' +branch_labels = None +depends_on = None + + +def upgrade(): +# ### commands auto generated by Alembic - please adjust! ### + op.drop_table('rfp_proposal') + 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. + now = datetime.now() + connection = op.get_bind() + connection.execute("UPDATE rfp SET date_closed = now() WHERE status = 'CLOSED'") + connection.execute("UPDATE rfp SET date_opened = now() WHERE status = 'LIVE'") + + + +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 ### From 1d1f3bb00792274dda25a45d71a70c642167cc93 Mon Sep 17 00:00:00 2001 From: Will O'Beirne Date: Fri, 8 Feb 2019 11:54:20 -0500 Subject: [PATCH 2/8] Admin interface for new fields. --- admin/src/components/RFPForm/index.less | 20 +++++++ admin/src/components/RFPForm/index.tsx | 74 ++++++++++++++++++++++--- admin/src/types.ts | 10 +++- backend/grant/admin/views.py | 4 +- backend/grant/rfp/models.py | 18 +++++- 5 files changed, 113 insertions(+), 13 deletions(-) diff --git a/admin/src/components/RFPForm/index.less b/admin/src/components/RFPForm/index.less index 40df07c9..ca925f5e 100644 --- a/admin/src/components/RFPForm/index.less +++ b/admin/src/components/RFPForm/index.less @@ -1,4 +1,6 @@ .RFPForm { + max-width: 880px; + &-content { &-preview { 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 { .ant-btn { margin-right: 0.5rem; diff --git a/admin/src/components/RFPForm/index.tsx b/admin/src/components/RFPForm/index.tsx index 37c03b8f..eec42a85 100644 --- a/admin/src/components/RFPForm/index.tsx +++ b/admin/src/components/RFPForm/index.tsx @@ -1,7 +1,9 @@ import React from 'react'; +import moment, { Moment } from 'moment'; import { view } from 'react-easy-state'; 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 { FormComponentProps } from 'antd/lib/form'; import { PROPOSAL_CATEGORY, RFP_STATUS, RFPArgs } from 'src/types'; @@ -42,6 +44,9 @@ class RFPForm extends React.Component { content: '', category: '', status: '', + matching: false, + bounty: undefined, + dateCloses: undefined, }; const rfpId = this.getRFPId(); if (rfpId) { @@ -57,12 +62,18 @@ class RFPForm extends React.Component { content: rfp.content, category: rfp.category, status: rfp.status, + matching: rfp.matching, + bounty: rfp.bounty, + dateCloses: rfp.dateCloses || undefined, }; } else { return ; } } + const dateCloses: Moment = getFieldValue('dateCloses'); + const forceClosed = dateCloses && dateCloses.isBefore(moment.now()); + return (
@@ -85,12 +96,15 @@ class RFPForm extends React.Component { {rfpId && ( - + {getFieldDecorator('status', { initialValue: defaults.status, rules: [{ required: true, message: 'Status is required' }], })( - {typedKeys(RFP_STATUS).map(c => ( {getStatusById(RFP_STATUSES, c).tagDisplay} @@ -164,13 +178,55 @@ class RFPForm extends React.Component { )} + + + + {getFieldDecorator('bounty', { + initialValue: defaults.bounty, + })( + , + )} + {getFieldDecorator('matching')( + + Match community contributions for approved proposals + , + )} + + + + + {getFieldDecorator('dateCloses', { + initialValue: defaults.dateCloses ? moment(defaults.dateCloses * 1000) : undefined, + })( + + )} + + + +
- + + +
); @@ -189,10 +245,14 @@ class RFPForm extends React.Component { private handleSubmit = (ev: React.FormEvent) => { ev.preventDefault(); - this.props.form.validateFieldsAndScroll(async (err: any, values: any) => { + this.props.form.validateFieldsAndScroll(async (err: any, rawValues: any) => { if (err) return; const rfpId = this.getRFPId(); + const values = { + ...rawValues, + dateCloses: rawValues.dateCloses.unix(), + }; let msg; if (rfpId) { await store.editRFP(rfpId, values); diff --git a/admin/src/types.ts b/admin/src/types.ts index eb634d3b..32233d77 100644 --- a/admin/src/types.ts +++ b/admin/src/types.ts @@ -22,19 +22,27 @@ export enum RFP_STATUS { export interface RFP { id: number; dateCreated: number; + dateOpened: number | null; + dateClosed: number | null; title: string; brief: string; content: string; category: string; status: string; proposals: Proposal[]; + matching: boolean; + bounty: string | null; + dateCloses: number | null; } export interface RFPArgs { title: string; brief: string; content: 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 ProposalStatus export enum PROPOSAL_STATUS { diff --git a/backend/grant/admin/views.py b/backend/grant/admin/views.py index 19a88378..575ea6ad 100644 --- a/backend/grant/admin/views.py +++ b/backend/grant/admin/views.py @@ -315,7 +315,7 @@ def get_rfp(rfp_id): parameter('content', type=str), parameter('category', type=str), parameter('bounty', type=str), - parameter('matching', type=bool), + parameter('matching', type=bool, default=False), parameter('dateCloses', type=int), parameter('status', type=str), ) @@ -332,7 +332,7 @@ def update_rfp(rfp_id, title, brief, content, category, bounty, matching, date_c rfp.category = category rfp.bounty = bounty rfp.matching = matching - rfp.date_closes = date_closes + rfp.date_closes = datetime.fromtimestamp(date_closes) if date_closes else None # Update timestamps if status changed if rfp.status != status: diff --git a/backend/grant/rfp/models.py b/backend/grant/rfp/models.py index fec9d07e..e027daec 100644 --- a/backend/grant/rfp/models.py +++ b/backend/grant/rfp/models.py @@ -93,27 +93,39 @@ class AdminRFPSchema(ma.Schema): "content", "category", "status", + "matching", + "bounty", "date_created", + "date_closes", + "date_opened", + "date_closed", "proposals", ) + status = ma.Method("get_status") 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"]) + 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): return dt_to_unix(obj.date_created) def get_date_closes(self, obj): - return dt_to_unix(obj.date_closes) + 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) + 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) + return dt_to_unix(obj.date_closes) if obj.date_closes else None admin_rfp_schema = AdminRFPSchema() admin_rfps_schema = AdminRFPSchema(many=True) From 64da535650ebc2f387c8532d3434ee4c62d0d08c Mon Sep 17 00:00:00 2001 From: Will O'Beirne Date: Fri, 8 Feb 2019 11:59:52 -0500 Subject: [PATCH 3/8] Fix status lock --- admin/src/components/RFPForm/index.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/admin/src/components/RFPForm/index.tsx b/admin/src/components/RFPForm/index.tsx index eec42a85..342c966b 100644 --- a/admin/src/components/RFPForm/index.tsx +++ b/admin/src/components/RFPForm/index.tsx @@ -36,7 +36,7 @@ class RFPForm extends React.Component { render() { const { isShowingPreview } = this.state; - const { getFieldDecorator, getFieldValue } = this.props.form; + const { getFieldDecorator, getFieldValue, isFieldsTouched } = this.props.form; let defaults: RFPArgs = { title: '', @@ -70,8 +70,10 @@ class RFPForm extends React.Component { return ; } } - - const dateCloses: Moment = getFieldValue('dateCloses'); + + const dateCloses = isFieldsTouched(['dateCloses']) + ? getFieldValue('dateCloses') + : defaults.dateCloses && moment(defaults.dateCloses * 1000); const forceClosed = dateCloses && dateCloses.isBefore(moment.now()); return ( @@ -98,7 +100,7 @@ class RFPForm extends React.Component { {rfpId && ( {getFieldDecorator('status', { initialValue: defaults.status, From 263764255b867c2f07b4b11c29db686759acbb64 Mon Sep 17 00:00:00 2001 From: Will O'Beirne Date: Fri, 8 Feb 2019 14:02:34 -0500 Subject: [PATCH 4/8] Frontend for new RFP fields. --- admin/src/components/RFPForm/index.tsx | 2 +- backend/grant/admin/views.py | 11 ++++++--- backend/grant/proposal/views.py | 4 +-- backend/grant/rfp/models.py | 26 +++++++++++++++++--- backend/migrations/versions/d39bb526eef4_.py | 5 ++-- frontend/client/components/RFP/index.less | 14 +++++++++++ frontend/client/components/RFP/index.tsx | 23 ++++++++++++++++- frontend/client/components/RFPs/RFPItem.tsx | 14 +++++++++-- frontend/client/utils/api.ts | 3 +++ frontend/types/rfp.ts | 7 +++++- 10 files changed, 92 insertions(+), 17 deletions(-) diff --git a/admin/src/components/RFPForm/index.tsx b/admin/src/components/RFPForm/index.tsx index 342c966b..8155ae2c 100644 --- a/admin/src/components/RFPForm/index.tsx +++ b/admin/src/components/RFPForm/index.tsx @@ -253,7 +253,7 @@ class RFPForm extends React.Component { const rfpId = this.getRFPId(); const values = { ...rawValues, - dateCloses: rawValues.dateCloses.unix(), + dateCloses: rawValues.dateCloses && rawValues.dateCloses.unix(), }; let msg; if (rfpId) { diff --git a/backend/grant/admin/views.py b/backend/grant/admin/views.py index 575ea6ad..aac84530 100644 --- a/backend/grant/admin/views.py +++ b/backend/grant/admin/views.py @@ -286,12 +286,15 @@ def get_rfps(): parameter('content', type=str), parameter('category', type=str), parameter('bounty', type=str), - parameter('matching', type=bool), + parameter('matching', type=bool, default=False), parameter('dateCloses', type=int), ) @admin_auth_required -def create_rfp(**kwargs): - rfp = RFP(**kwargs) +def create_rfp(date_closes, **kwargs): + rfp = RFP( + **kwargs, + date_closes=datetime.fromtimestamp(date_closes) if date_closes else None, + ) db.session.add(rfp) db.session.commit() return admin_rfp_schema.dump(rfp), 201 @@ -336,7 +339,7 @@ def update_rfp(rfp_id, title, brief, content, category, bounty, matching, date_c # Update timestamps if status changed if rfp.status != status: - if status == RFPStatus.LIVE: + if status == RFPStatus.LIVE and not rfp.date_opened: rfp.date_opened = datetime.now() if status == RFPStatus.CLOSED: rfp.date_closed = datetime.now() diff --git a/backend/grant/proposal/views.py b/backend/grant/proposal/views.py index 16aeb148..4ac96171 100644 --- a/backend/grant/proposal/views.py +++ b/backend/grant/proposal/views.py @@ -161,9 +161,9 @@ def make_proposal_draft(rfp_id): rfp = RFP.query.filter_by(id=rfp_id).first() if not rfp: return {"message": "The request this proposal was made for doesn’t exist"}, 400 - proposal.title = rfp.title - proposal.brief = rfp.brief proposal.category = rfp.category + if rfp.matching: + proposal.matching = 1.0 rfp.proposals.append(proposal) db.session.add(rfp) diff --git a/backend/grant/rfp/models.py b/backend/grant/rfp/models.py index e027daec..4861ae50 100644 --- a/backend/grant/rfp/models.py +++ b/backend/grant/rfp/models.py @@ -68,15 +68,35 @@ class RFPSchema(ma.Schema): "content", "category", "status", + "matching", + "bounty", "date_created", + "date_closes", + "date_opened", + "date_closed", "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"]) - def get_date_created(self, obj): - return dt_to_unix(obj.date_created) + 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_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() rfps_schema = RFPSchema(many=True) diff --git a/backend/migrations/versions/d39bb526eef4_.py b/backend/migrations/versions/d39bb526eef4_.py index f89f9444..1131955b 100644 --- a/backend/migrations/versions/d39bb526eef4_.py +++ b/backend/migrations/versions/d39bb526eef4_.py @@ -8,7 +8,7 @@ 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 +from datetime import datetime, timedelta # revision identifiers, used by Alembic. @@ -29,10 +29,9 @@ def upgrade(): # ### end Alembic commands ### # Set columns for times based on status. - now = datetime.now() 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'") - connection.execute("UPDATE rfp SET date_opened = now() WHERE status = 'LIVE'") diff --git a/frontend/client/components/RFP/index.less b/frontend/client/components/RFP/index.less index d06845a1..b9ae68f2 100644 --- a/frontend/client/components/RFP/index.less +++ b/frontend/client/components/RFP/index.less @@ -32,9 +32,23 @@ &-content { font-size: 1.15rem; + margin-bottom: 2rem; + } + + &-rules { padding-bottom: 2rem; margin-bottom: 2rem; 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 { diff --git a/frontend/client/components/RFP/index.tsx b/frontend/client/components/RFP/index.tsx index 77b21bd3..60130468 100644 --- a/frontend/client/components/RFP/index.tsx +++ b/frontend/client/components/RFP/index.tsx @@ -11,6 +11,7 @@ import { AppState } from 'store/reducers'; import Loader from 'components/Loader'; import Markdown from 'components/Markdown'; import ProposalCard from 'components/Proposals/ProposalCard'; +import UnitDisplay from 'components/UnitDisplay'; import './index.less'; interface OwnProps { @@ -53,11 +54,31 @@ class RFPDetail extends React.Component { Back to Requests
- Opened {moment(rfp.dateCreated * 1000).format('LL')} + Opened {moment(rfp.dateOpened * 1000).format('LL')}

{rfp.title}

+
+
    + {rfp.bounty && ( +
  • + Accepted proposals will be funded up to{' '} + +
  • + )} + {rfp.matching && ( +
  • + Contributions will have their funding matched by the Zcash Foundation +
  • + )} + {rfp.dateCloses && ( +
  • + Proposal submissions end {moment(rfp.dateCloses * 1000).format('LL')} +
  • + )} +
+
{!!rfp.acceptedProposals.length && (
diff --git a/frontend/client/components/RFPs/RFPItem.tsx b/frontend/client/components/RFPs/RFPItem.tsx index ab738d1f..3db35a6a 100644 --- a/frontend/client/components/RFPs/RFPItem.tsx +++ b/frontend/client/components/RFPs/RFPItem.tsx @@ -13,7 +13,16 @@ interface Props { export default class RFPItem extends React.Component { render() { const { rfp, isSmall } = this.props; - const { id, title, brief, acceptedProposals, dateCreated } = rfp; + const { + id, + title, + brief, + acceptedProposals, + dateOpened, + dateCloses, + dateClosed, + } = rfp; + const closeDate = dateCloses || dateClosed; return ( {
- {moment(dateCreated * 1000).format('LL')} + {moment(dateOpened * 1000).format('LL')} + {closeDate && <> – {moment(closeDate * 1000).format('LL')}}
{acceptedProposals.length} proposals approved diff --git a/frontend/client/utils/api.ts b/frontend/client/utils/api.ts index 3170b492..31801053 100644 --- a/frontend/client/utils/api.ts +++ b/frontend/client/utils/api.ts @@ -103,6 +103,9 @@ export function formatProposalFromGet(p: any): Proposal { } export function formatRFPFromGet(rfp: RFP): RFP { + if (rfp.bounty) { + rfp.bounty = toZat(rfp.bounty as any); + } rfp.acceptedProposals = rfp.acceptedProposals.map(formatProposalFromGet); return rfp; } diff --git a/frontend/types/rfp.ts b/frontend/types/rfp.ts index 00cdb79e..76035a54 100644 --- a/frontend/types/rfp.ts +++ b/frontend/types/rfp.ts @@ -1,13 +1,18 @@ import { Proposal } from './proposal'; import { PROPOSAL_CATEGORY, RFP_STATUS } from 'api/constants'; +import { Zat } from 'utils/units'; export interface RFP { id: number; - dateCreated: number; title: string; brief: string; content: string; category: PROPOSAL_CATEGORY; status: RFP_STATUS; acceptedProposals: Proposal[]; + bounty: Zat | null; + matching: boolean; + dateOpened: number; + dateClosed?: number; + dateCloses?: number; } From 74fba18f99d798420dde81fcf5c0c6efc9cc8bf4 Mon Sep 17 00:00:00 2001 From: Will O'Beirne Date: Fri, 8 Feb 2019 14:11:52 -0500 Subject: [PATCH 5/8] Fix contribution matching. Add deets to rfp detail admin. --- admin/src/components/RFPDetail/index.tsx | 3 +++ backend/grant/proposal/views.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/admin/src/components/RFPDetail/index.tsx b/admin/src/components/RFPDetail/index.tsx index 56a74a5d..d6c5c8df 100644 --- a/admin/src/components/RFPDetail/index.tsx +++ b/admin/src/components/RFPDetail/index.tsx @@ -91,6 +91,9 @@ class RFPDetail extends React.Component { {renderDeetItem('created', formatDateSeconds(rfp.dateCreated))} {renderDeetItem('status', rfp.status)} {renderDeetItem('category', rfp.category)} + {renderDeetItem('matching', String(rfp.matching))} + {renderDeetItem('bounty', `${rfp.bounty} ZEC`)} + {renderDeetItem('dateCloses', rfp.dateCloses && formatDateSeconds(rfp.dateCloses))} {/* PROPOSALS */} diff --git a/backend/grant/proposal/views.py b/backend/grant/proposal/views.py index 4ac96171..eccee99c 100644 --- a/backend/grant/proposal/views.py +++ b/backend/grant/proposal/views.py @@ -163,7 +163,7 @@ def make_proposal_draft(rfp_id): return {"message": "The request this proposal was made for doesn’t exist"}, 400 proposal.category = rfp.category if rfp.matching: - proposal.matching = 1.0 + proposal.contribution_matching = 1.0 rfp.proposals.append(proposal) db.session.add(rfp) From 030ac6439131adeaf05b8b0dde9a022975777843 Mon Sep 17 00:00:00 2001 From: Will O'Beirne Date: Thu, 14 Feb 2019 19:08:48 -0500 Subject: [PATCH 6/8] Fix migration --- backend/migrations/versions/d39bb526eef4_.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/migrations/versions/d39bb526eef4_.py b/backend/migrations/versions/d39bb526eef4_.py index 1131955b..fecd583e 100644 --- a/backend/migrations/versions/d39bb526eef4_.py +++ b/backend/migrations/versions/d39bb526eef4_.py @@ -1,7 +1,7 @@ """Adds RFP bounty, matching, and date fields Revision ID: d39bb526eef4 -Revises: 310dca400b81 +Revises: 3793d9a71e27 Create Date: 2019-02-07 15:09:11.548655 """ @@ -13,14 +13,13 @@ from datetime import datetime, timedelta # revision identifiers, used by Alembic. revision = 'd39bb526eef4' -down_revision = '310dca400b81' +down_revision = '3793d9a71e27' branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('rfp_proposal') 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)) From 170510e1b8ad073fb1ebbdebd5d231773b88c6eb Mon Sep 17 00:00:00 2001 From: Will O'Beirne Date: Thu, 14 Feb 2019 19:14:03 -0500 Subject: [PATCH 7/8] Set initialValue of matching field on RFPs. --- admin/src/components/RFPForm/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/admin/src/components/RFPForm/index.tsx b/admin/src/components/RFPForm/index.tsx index 8155ae2c..29fe0dbf 100644 --- a/admin/src/components/RFPForm/index.tsx +++ b/admin/src/components/RFPForm/index.tsx @@ -194,7 +194,9 @@ class RFPForm extends React.Component { size="large" />, )} - {getFieldDecorator('matching')( + {getFieldDecorator('matching', { + initialValue: defaults.matching, + })( Date: Fri, 15 Feb 2019 12:46:50 -0500 Subject: [PATCH 8/8] Tags for list view, bold bounty and matching on detail view. --- frontend/client/components/RFP/index.tsx | 7 +++-- frontend/client/components/RFPs/RFPItem.less | 5 ++++ frontend/client/components/RFPs/RFPItem.tsx | 27 +++++++++++++++++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/frontend/client/components/RFP/index.tsx b/frontend/client/components/RFP/index.tsx index 60130468..3600ac3c 100644 --- a/frontend/client/components/RFP/index.tsx +++ b/frontend/client/components/RFP/index.tsx @@ -64,12 +64,15 @@ class RFPDetail extends React.Component { {rfp.bounty && (
  • Accepted proposals will be funded up to{' '} - + + +
  • )} {rfp.matching && (
  • - Contributions will have their funding matched by the Zcash Foundation + Contributions will have their funding matched by the + Zcash Foundation
  • )} {rfp.dateCloses && ( diff --git a/frontend/client/components/RFPs/RFPItem.less b/frontend/client/components/RFPs/RFPItem.less index c77d3af8..16daad19 100644 --- a/frontend/client/components/RFPs/RFPItem.less +++ b/frontend/client/components/RFPs/RFPItem.less @@ -24,6 +24,11 @@ font-size: 1.4rem; margin-bottom: 0.5rem; color: @primary-color; + + .ant-tag { + vertical-align: top; + margin: 0.5rem 0 0 0.5rem; + } } &-brief { diff --git a/frontend/client/components/RFPs/RFPItem.tsx b/frontend/client/components/RFPs/RFPItem.tsx index 3db35a6a..7645b3d5 100644 --- a/frontend/client/components/RFPs/RFPItem.tsx +++ b/frontend/client/components/RFPs/RFPItem.tsx @@ -1,7 +1,9 @@ import React from 'react'; import moment from 'moment'; import classnames from 'classnames'; +import { Tag } from 'antd'; import { Link } from 'react-router-dom'; +import UnitDisplay from 'components/UnitDisplay'; import { RFP } from 'types'; import './RFPItem.less'; @@ -21,15 +23,38 @@ export default class RFPItem extends React.Component { dateOpened, dateCloses, dateClosed, + bounty, + matching, } = rfp; const closeDate = dateCloses || dateClosed; + const tags = []; + if (!isSmall) { + if (bounty) { + tags.push( + + bounty + , + ); + } + if (matching) { + tags.push( + + x2 matching + , + ); + } + } + return ( -

    {title}

    +

    + {title} + {tags} +

    {brief}