proposal arbiter BE basics + initial admin work

This commit is contained in:
Aaron 2019-02-05 14:45:26 -06:00
parent 6310e7ca90
commit 2868d93fa1
No known key found for this signature in database
GPG Key ID: 3B5B7597106F0A0E
9 changed files with 108 additions and 12 deletions

View File

@ -7,6 +7,10 @@
margin-bottom: 3rem;
font-size: 1rem;
& > * + * {
margin-top: 1rem;
}
.anticon {
color: #ffaa00;
}

View File

@ -11,17 +11,39 @@ class Home extends React.Component {
}
render() {
const { userCount, proposalCount, proposalPendingCount } = store.stats;
const {
userCount,
proposalCount,
proposalPendingCount,
proposalNoArbiterCount,
} = store.stats;
const actionItems = [
!!proposalPendingCount && (
<div>
<Icon type="exclamation-circle" /> There are <b>{proposalPendingCount}</b>{' '}
proposals <b>waiting for review</b>.{' '}
<Link to="/proposals?status=PENDING">Click here</Link> to view them.
</div>
),
!!proposalNoArbiterCount && (
<div>
<Icon type="exclamation-circle" /> There are <b>{proposalNoArbiterCount}</b>{' '}
live proposals <b>without an arbitor</b>. No one can approve their payout
requests! <Link to="/proposals?status=LIVE&arbiter=false">Click here</Link> to
view them.
</div>
),
].filter(Boolean);
return (
<div className="Home">
{!!proposalPendingCount && (
{!!actionItems.length && (
<div className="Home-actionItems">
<Divider orientation="left">Action Items</Divider>
<div>
<Icon type="exclamation-circle" /> There are <b>{proposalPendingCount}</b>{' '}
proposals waiting for review.{' '}
<Link to="/proposals?status=PENDING">Click here</Link> to view them.
</div>
{actionItems.map((ai, i) => (
<div key={i}>{ai}</div>
))}
</div>
)}

View File

@ -197,6 +197,29 @@ class ProposalDetailNaked extends React.Component<Props, State> {
/>
);
const renderSetArbiter = () =>
!p.arbiter &&
p.status === PROPOSAL_STATUS.LIVE && (
<Alert
showIcon
type="warning"
message="No Arbiter on Live Proposal"
description={
<div>
<p>An arbiter is required to review milestone payout requests.</p>
<Button
loading={store.proposalDetailApproving}
icon="crown"
type="primary"
onClick={this.handleApprove}
>
Set arbiter
</Button>
</div>
}
/>
);
const renderDeetItem = (name: string, val: any) => (
<div className="ProposalDetail-deet">
<span>{name}</span>
@ -214,6 +237,7 @@ class ProposalDetailNaked extends React.Component<Props, State> {
{renderApproved()}
{renderReview()}
{renderRejected()}
{renderSetArbiter()}
<Collapse defaultActiveKey={['brief', 'content']}>
<Collapse.Panel key="brief" header="brief">
{p.brief}
@ -250,8 +274,10 @@ class ProposalDetailNaked extends React.Component<Props, State> {
{renderDeetItem('funded (inc. matching)', p.funded)}
{renderDeetItem('matching', p.contributionMatching)}
{p.rfp &&
renderDeetItem('rfp', <Link to={`/rfps/${p.rfp.id}`}>{p.rfp.title}</Link>)
}
renderDeetItem(
'rfp',
<Link to={`/rfps/${p.rfp.id}`}>{p.rfp.title}</Link>,
)}
</Card>
{/* TEAM */}

View File

@ -112,6 +112,7 @@ const app = store({
userCount: 0,
proposalCount: 0,
proposalPendingCount: 0,
proposalNoArbiterCount: 0,
},
usersFetching: false,

View File

@ -68,6 +68,7 @@ export interface Proposal {
rejectReason: string;
contributionMatching: number;
rfp?: RFP;
arbiter?: User;
}
export interface Comment {
commentId: string;

View File

@ -56,10 +56,15 @@ def stats():
proposal_pending_count = db.session.query(func.count(Proposal.id)) \
.filter(Proposal.status == ProposalStatus.PENDING) \
.scalar()
proposal_no_arbiter_count = db.session.query(func.count(Proposal.id)) \
.filter(Proposal.status == ProposalStatus.LIVE) \
.filter(Proposal.arbiter_id == None) \
.scalar()
return {
"userCount": user_count,
"proposalCount": proposal_count,
"proposalPendingCount": proposal_pending_count,
"proposalNoArbiterCount": proposal_no_arbiter_count,
}

View File

@ -115,6 +115,7 @@ class Proposal(db.Model):
id = db.Column(db.Integer(), primary_key=True)
date_created = db.Column(db.DateTime)
rfp_id = db.Column(db.Integer(), db.ForeignKey('rfp.id'), nullable=True)
arbiter_id = db.Column(db.Integer(), db.ForeignKey('user.id'), nullable=True)
# Content info
status = db.Column(db.String(255), nullable=False)
@ -141,6 +142,7 @@ class Proposal(db.Model):
contributions = db.relationship(ProposalContribution, backref="proposal", lazy=True, cascade="all, delete-orphan")
milestones = db.relationship("Milestone", backref="proposal", lazy=True, cascade="all, delete-orphan")
invites = db.relationship(ProposalTeamInvite, backref="proposal", lazy=True, cascade="all, delete-orphan")
arbiter = db.relationship("User", lazy=True, back_populates="arbitrated_proposals")
def __init__(
self,
@ -359,7 +361,8 @@ class ProposalSchema(ma.Schema):
"deadline_duration",
"contribution_matching",
"invites",
"rfp"
"rfp",
"arbiter"
)
date_created = ma.Method("get_date_created")
@ -373,6 +376,7 @@ class ProposalSchema(ma.Schema):
milestones = ma.Nested("MilestoneSchema", many=True)
invites = ma.Nested("ProposalTeamInviteSchema", many=True)
rfp = ma.Nested("RFPSchema", exclude=["accepted_proposals"])
arbiter = ma.Nested("UserSchema") # exclude=["arbitrated_proposals"])
def get_proposal_id(self, obj):
return obj.id

View File

@ -18,7 +18,7 @@ from sqlalchemy.ext.hybrid import hybrid_property
def is_current_authed_user_id(user_id):
return current_user.is_authenticated and \
current_user.id == user_id
current_user.id == user_id
class RolesUsers(db.Model):
@ -118,6 +118,7 @@ class User(db.Model, UserMixin):
lazy=True, cascade="all, delete-orphan")
roles = db.relationship('Role', secondary='roles_users',
backref=db.backref('users', lazy='dynamic'))
arbitrated_proposals = db.relationship("Proposal", lazy=True, back_populates="arbiter")
# TODO - add create and validate methods
@ -235,13 +236,15 @@ class SelfUserSchema(ma.Schema):
"avatar",
"display_name",
"userid",
"email_verified"
"email_verified",
"arbitrated_proposals"
)
social_medias = ma.Nested("SocialMediaSchema", many=True)
avatar = ma.Nested("AvatarSchema")
userid = ma.Method("get_userid")
email_verified = ma.Method("get_email_verified")
arbitrated_proposals = ma.Nested("Proposals", many=True, exclude=["arbitor"])
def get_userid(self, obj):
return obj.id

View File

@ -0,0 +1,30 @@
"""empty message
Revision ID: 310dca400b81
Revises: fa1fedf4ca08
Create Date: 2019-02-05 11:24:11.291158
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '310dca400b81'
down_revision = 'fa1fedf4ca08'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('proposal', sa.Column('arbiter_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'proposal', 'user', ['arbiter_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'proposal', type_='foreignkey')
op.drop_column('proposal', 'arbiter_id')
# ### end Alembic commands ###