From 625a6cab990f805aceb6fe42771a8c2ed7260498 Mon Sep 17 00:00:00 2001
From: Daniel Ternyak
- the refund process? This cannot be undone.
-
Created: {formatDateSeconds(p.dateCreated)}
{p.brief}
{p.rfp && ( -Submitted for RFP: {p.rfp.title}
++ Submitted for RFP: {p.rfp.title} +
)}- Zcash Foundation, 123 Address Street, Somewhere, NY 11211 + Zcash Foundation + 1390 Chain Bridge Road, #A132 + McLean, VA 22101
/verify", methods=["POST"])
-@endpoint.api()
def verify_email(code):
ev = EmailVerification.query.filter_by(code=code).first()
if ev:
@@ -20,7 +17,6 @@ def verify_email(code):
@blueprint.route("//unsubscribe", methods=["POST"])
-@endpoint.api()
def unsubscribe_email(code):
ev = EmailVerification.query.filter_by(code=code).first()
if ev:
@@ -32,7 +28,6 @@ def unsubscribe_email(code):
@blueprint.route("//arbiter/", methods=["POST"])
-@endpoint.api()
def accept_arbiter(code, proposal_id):
ev = EmailVerification.query.filter_by(code=code).first()
if ev:
diff --git a/backend/grant/milestone/views.py b/backend/grant/milestone/views.py
index 751652b7..99d079d4 100644
--- a/backend/grant/milestone/views.py
+++ b/backend/grant/milestone/views.py
@@ -1,14 +1,4 @@
from flask import Blueprint
-from flask_yoloapi import endpoint
-
-from .models import Milestone, milestones_schema
blueprint = Blueprint('milestone', __name__, url_prefix='/api/v1/milestones')
-# Unused
-# @blueprint.route("/", methods=["GET"])
-# @endpoint.api()
-# def get_milestones():
-# milestones = Milestone.query.all()
-# result = milestones_schema.dump(milestones)
-# return result
diff --git a/backend/grant/parser.py b/backend/grant/parser.py
new file mode 100644
index 00000000..dd39013b
--- /dev/null
+++ b/backend/grant/parser.py
@@ -0,0 +1,91 @@
+import functools
+
+from animal_case import animalify
+from webargs.core import dict2schema
+from webargs.flaskparser import FlaskParser, abort
+from marshmallow import fields
+
+try:
+ from collections.abc import Mapping
+except ImportError:
+ from collections import Mapping
+
+
+class Parser(FlaskParser):
+ DEFAULT_VALIDATION_STATUS = 400
+
+ def use_kwargs(self, *args, **kwargs):
+
+ kwargs["as_kwargs"] = True
+ return self.use_args(*args, **kwargs)
+
+ def use_args(
+ self,
+ argmap,
+ req=None,
+ locations=None,
+ as_kwargs=False,
+ validate=None,
+ error_status_code=None,
+ error_headers=None,
+ ):
+ locations = locations or self.locations
+ request_obj = req
+ # Optimization: If argmap is passed as a dictionary, we only need
+ # to generate a Schema once
+ if isinstance(argmap, Mapping):
+ argmap = dict2schema(argmap)()
+
+ def decorator(func):
+ req_ = request_obj
+
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ req_obj = req_
+
+ if not req_obj:
+ req_obj = self.get_request_from_view_args(func, args, kwargs)
+ # NOTE: At this point, argmap may be a Schema, or a callable
+ parsed_args = self.parse(
+ argmap,
+ req=req_obj,
+ locations=locations,
+ validate=validate,
+ error_status_code=error_status_code,
+ error_headers=error_headers,
+ )
+ if as_kwargs:
+ # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ # ONLY CHANGE FROM ORIGINAL
+ kwargs.update(animalify(parsed_args, types='snake'))
+ # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ return func(*args, **kwargs)
+ else:
+ # Add parsed_args after other positional arguments
+ new_args = args + (parsed_args,)
+ return func(*new_args, **kwargs)
+
+ wrapper.__wrapped__ = func
+ return wrapper
+
+ return decorator
+
+ def handle_invalid_json_error(self, error, req, *args, **kwargs):
+ print(error)
+ abort(400, exc=error, messages={"json": ["Invalid JSON body."]})
+
+
+parser = Parser()
+use_args = parser.use_args
+use_kwargs = parser.use_kwargs
+
+# default kwargs
+query = functools.partial(use_kwargs, locations=("query",))
+body = functools.partial(use_kwargs, locations=("json",))
+
+paginated_fields = {
+ "page": fields.Int(required=False, missing=None),
+ "filters": fields.List(fields.Str(), required=False, missing=None),
+ "search": fields.Str(required=False, missing=None),
+ "sort": fields.Str(required=False, missing=None)
+}
\ No newline at end of file
diff --git a/backend/grant/proposal/views.py b/backend/grant/proposal/views.py
index e0cd06b1..12164449 100644
--- a/backend/grant/proposal/views.py
+++ b/backend/grant/proposal/views.py
@@ -1,13 +1,19 @@
-from dateutil.parser import parse
+from datetime import datetime
from decimal import Decimal
+
from flask import Blueprint, g, request
-from flask_yoloapi import endpoint, parameter
+from marshmallow import fields, validate
+from sqlalchemy import or_
+
from grant.comment.models import Comment, comment_schema, comments_schema
from grant.email.send import send_email
from grant.milestone.models import Milestone
-from grant.settings import EXPLORER_URL, PROPOSAL_STAKING_AMOUNT
-from grant.user.models import User
+from grant.parser import body, query, paginated_fields
from grant.rfp.models import RFP
+from grant.settings import EXPLORER_URL, PROPOSAL_STAKING_AMOUNT
+from grant.task.jobs import ProposalDeadline
+from grant.user.models import User
+from grant.utils import pagination
from grant.utils.auth import (
requires_auth,
requires_team_member_auth,
@@ -16,14 +22,10 @@ from grant.utils.auth import (
get_authed_user,
internal_webhook
)
+from grant.utils.enums import Category
+from grant.utils.enums import ProposalStatus, ProposalStage, ContributionStatus
from grant.utils.exceptions import ValidationException
from grant.utils.misc import is_email, make_url, from_zat
-from grant.utils.enums import ProposalStatus, ProposalStage, ContributionStatus
-from grant.utils import pagination
-from grant.task.jobs import ProposalDeadline
-from sqlalchemy import or_
-from datetime import datetime
-
from .models import (
Proposal,
proposals_schema,
@@ -43,7 +45,6 @@ blueprint = Blueprint("proposal", __name__, url_prefix="/api/v1/proposals")
@blueprint.route("/", methods=["GET"])
-@endpoint.api()
def get_proposal(proposal_id):
proposal = Proposal.query.filter_by(id=proposal_id).first()
if proposal:
@@ -60,12 +61,7 @@ def get_proposal(proposal_id):
@blueprint.route("//comments", methods=["GET"])
-@endpoint.api(
- parameter('page', type=int, required=False),
- parameter('filters', type=list, required=False),
- parameter('search', type=str, required=False),
- parameter('sort', type=str, required=False)
-)
+@query(paginated_fields)
def get_proposal_comments(proposal_id, page, filters, search, sort):
# only using page, currently
filters_workaround = request.args.getlist('filters[]')
@@ -82,7 +78,6 @@ def get_proposal_comments(proposal_id, page, filters, search, sort):
@blueprint.route("//comments//report", methods=["PUT"])
@requires_email_verified_auth
-@endpoint.api()
def report_proposal_comment(proposal_id, comment_id):
# Make sure proposal exists
proposal = Proposal.query.filter_by(id=proposal_id).first()
@@ -95,15 +90,15 @@ def report_proposal_comment(proposal_id, comment_id):
comment.report(True)
db.session.commit()
- return None, 200
+ return {"message": "ok"}, 200
@blueprint.route("//comments", methods=["POST"])
@requires_email_verified_auth
-@endpoint.api(
- parameter('comment', type=str, required=True),
- parameter('parentCommentId', type=int, required=False)
-)
+@body({
+ "comment": fields.Str(required=True),
+ "parentCommentId": fields.Int(required=False, missing=None),
+})
def post_proposal_comments(proposal_id, comment, parent_comment_id):
# Make sure proposal exists
proposal = Proposal.query.filter_by(id=proposal_id).first()
@@ -161,12 +156,7 @@ def post_proposal_comments(proposal_id, comment, parent_comment_id):
@blueprint.route("/", methods=["GET"])
-@endpoint.api(
- parameter('page', type=int, required=False),
- parameter('filters', type=list, required=False),
- parameter('search', type=str, required=False),
- parameter('sort', type=str, required=False)
-)
+@query(paginated_fields)
def get_proposals(page, filters, search, sort):
filters_workaround = request.args.getlist('filters[]')
query = Proposal.query.filter_by(status=ProposalStatus.LIVE) \
@@ -185,9 +175,9 @@ def get_proposals(page, filters, search, sort):
@blueprint.route("/drafts", methods=["POST"])
@requires_email_verified_auth
-@endpoint.api(
- parameter('rfpId', type=int),
-)
+@body({
+ "rfpId": fields.Int(required=False, missing=None)
+})
def make_proposal_draft(rfp_id):
proposal = Proposal.create(status=ProposalStatus.DRAFT)
proposal.team.append(g.current_user)
@@ -209,34 +199,34 @@ def make_proposal_draft(rfp_id):
@blueprint.route("/drafts", methods=["GET"])
@requires_auth
-@endpoint.api()
def get_proposal_drafts():
proposals = (
Proposal.query
- .filter(or_(
+ .filter(or_(
Proposal.status == ProposalStatus.DRAFT,
Proposal.status == ProposalStatus.REJECTED,
))
- .join(proposal_team)
- .filter(proposal_team.c.user_id == g.current_user.id)
- .order_by(Proposal.date_created.desc())
- .all()
+ .join(proposal_team)
+ .filter(proposal_team.c.user_id == g.current_user.id)
+ .order_by(Proposal.date_created.desc())
+ .all()
)
return proposals_schema.dump(proposals), 200
@blueprint.route("/", methods=["PUT"])
@requires_team_member_auth
-@endpoint.api(
- parameter('title', type=str),
- parameter('brief', type=str),
- parameter('category', type=str),
- parameter('content', type=str),
- parameter('target', type=str),
- parameter('payoutAddress', type=str),
- parameter('deadlineDuration', type=int),
- parameter('milestones', type=list)
-)
+# TODO add gaurd (minimum, maximum, shape)
+@body({
+ "title": fields.Str(required=True),
+ "brief": fields.Str(required=True),
+ "category": fields.Str(required=True, validate=validate.OneOf(choices=Category.list())),
+ "content": fields.Str(required=True),
+ "target": fields.Str(required=True),
+ "payoutAddress": fields.Str(required=True),
+ "deadlineDuration": fields.Int(required=True),
+ "milestones": fields.List(fields.Dict(), required=True),
+})
def update_proposal(milestones, proposal_id, **kwargs):
# Update the base proposal fields
try:
@@ -251,9 +241,9 @@ def update_proposal(milestones, proposal_id, **kwargs):
m = Milestone(
title=mdata["title"],
content=mdata["content"],
- date_estimated=datetime.fromtimestamp(mdata["dateEstimated"]),
- payout_percent=str(mdata["payoutPercent"]),
- immediate_payout=mdata["immediatePayout"],
+ date_estimated=datetime.fromtimestamp(mdata["date_estimated"]),
+ payout_percent=str(mdata["payout_percent"]),
+ immediate_payout=mdata["immediate_payout"],
proposal_id=g.current_proposal.id,
index=i
)
@@ -266,7 +256,6 @@ def update_proposal(milestones, proposal_id, **kwargs):
@blueprint.route("//rfp", methods=["DELETE"])
@requires_team_member_auth
-@endpoint.api()
def unlink_proposal_from_rfp(proposal_id):
g.current_proposal.rfp_id = None
db.session.add(g.current_proposal)
@@ -276,7 +265,6 @@ def unlink_proposal_from_rfp(proposal_id):
@blueprint.route("/", methods=["DELETE"])
@requires_team_member_auth
-@endpoint.api()
def delete_proposal(proposal_id):
deleteable_statuses = [
ProposalStatus.DRAFT,
@@ -290,12 +278,11 @@ def delete_proposal(proposal_id):
return {"message": "Cannot delete proposals with %s status" % status}, 400
db.session.delete(g.current_proposal)
db.session.commit()
- return None, 202
+ return {"message": "ok"}, 202
@blueprint.route("//submit_for_approval", methods=["PUT"])
@requires_team_member_auth
-@endpoint.api()
def submit_for_approval_proposal(proposal_id):
try:
g.current_proposal.submit_for_approval()
@@ -308,19 +295,17 @@ def submit_for_approval_proposal(proposal_id):
@blueprint.route("//stake", methods=["GET"])
@requires_team_member_auth
-@endpoint.api()
def get_proposal_stake(proposal_id):
if g.current_proposal.status != ProposalStatus.STAKING:
- return None, 400
+ return {"message": "ok"}, 400
contribution = g.current_proposal.get_staking_contribution(g.current_user.id)
if contribution:
return proposal_contribution_schema.dump(contribution)
- return None, 404
+ return {"message": "ok"}, 404
@blueprint.route("//publish", methods=["PUT"])
@requires_team_member_auth
-@endpoint.api()
def publish_proposal(proposal_id):
try:
g.current_proposal.publish()
@@ -336,7 +321,6 @@ def publish_proposal(proposal_id):
@blueprint.route("//updates", methods=["GET"])
-@endpoint.api()
def get_proposal_updates(proposal_id):
proposal = Proposal.query.filter_by(id=proposal_id).first()
if proposal:
@@ -347,7 +331,6 @@ def get_proposal_updates(proposal_id):
@blueprint.route("//updates/", methods=["GET"])
-@endpoint.api()
def get_proposal_update(proposal_id, update_id):
proposal = Proposal.query.filter_by(id=proposal_id).first()
if proposal:
@@ -362,10 +345,10 @@ def get_proposal_update(proposal_id, update_id):
@blueprint.route("//updates", methods=["POST"])
@requires_team_member_auth
-@endpoint.api(
- parameter('title', type=str, required=True),
- parameter('content', type=str, required=True)
-)
+@body({
+ "title": fields.Str(required=True),
+ "content": fields.Str(required=True)
+})
def post_proposal_update(proposal_id, title, content):
update = ProposalUpdate(
proposal_id=g.current_proposal.id,
@@ -391,9 +374,9 @@ def post_proposal_update(proposal_id, title, content):
@blueprint.route("//invite", methods=["POST"])
@requires_team_member_auth
-@endpoint.api(
- parameter('address', type=str, required=True)
-)
+@body({
+ "address": fields.Str(required=True),
+})
def post_proposal_team_invite(proposal_id, address):
invite = ProposalTeamInvite(
proposal_id=proposal_id,
@@ -422,7 +405,6 @@ def post_proposal_team_invite(proposal_id, address):
@blueprint.route("//invite/", methods=["DELETE"])
@requires_team_member_auth
-@endpoint.api()
def delete_proposal_team_invite(proposal_id, id_or_address):
invite = ProposalTeamInvite.query.filter(
(ProposalTeamInvite.id == id_or_address) |
@@ -435,11 +417,10 @@ def delete_proposal_team_invite(proposal_id, id_or_address):
db.session.delete(invite)
db.session.commit()
- return None, 202
+ return {"message": "ok"}, 202
@blueprint.route("//contributions", methods=["GET"])
-@endpoint.api()
def get_proposal_contributions(proposal_id):
proposal = Proposal.query.filter_by(id=proposal_id).first()
if not proposal:
@@ -447,19 +428,19 @@ def get_proposal_contributions(proposal_id):
top_contributions = ProposalContribution.query \
.filter_by(
- proposal_id=proposal_id,
- status=ContributionStatus.CONFIRMED,
- staking=False,
- ) \
+ proposal_id=proposal_id,
+ status=ContributionStatus.CONFIRMED,
+ staking=False,
+ ) \
.order_by(ProposalContribution.amount.desc()) \
.limit(5) \
.all()
latest_contributions = ProposalContribution.query \
.filter_by(
- proposal_id=proposal_id,
- status=ContributionStatus.CONFIRMED,
- staking=False,
- ) \
+ proposal_id=proposal_id,
+ status=ContributionStatus.CONFIRMED,
+ staking=False,
+ ) \
.order_by(ProposalContribution.date_created.desc()) \
.limit(5) \
.all()
@@ -471,7 +452,6 @@ def get_proposal_contributions(proposal_id):
@blueprint.route("//contributions/", methods=["GET"])
-@endpoint.api()
def get_proposal_contribution(proposal_id, contribution_id):
proposal = Proposal.query.filter_by(id=proposal_id).first()
if proposal:
@@ -485,10 +465,11 @@ def get_proposal_contribution(proposal_id, contribution_id):
@blueprint.route("//contributions", methods=["POST"])
-@endpoint.api(
- parameter('amount', type=str, required=True),
- parameter('anonymous', type=bool, required=False)
-)
+# TODO add gaurd (minimum, maximum)
+@body({
+ "amount": fields.Str(required=True),
+ "anonymous": fields.Bool(required=False, missing=None)
+})
def post_proposal_contribution(proposal_id, amount, anonymous):
proposal = Proposal.query.filter_by(id=proposal_id).first()
if not proposal:
@@ -518,11 +499,11 @@ def post_proposal_contribution(proposal_id, amount, anonymous):
# Can't use since webhook doesn't know proposal id
@blueprint.route("/contribution//confirm", methods=["POST"])
@internal_webhook
-@endpoint.api(
- parameter('to', type=str, required=True),
- parameter('amount', type=str, required=True),
- parameter('txid', type=str, required=True),
-)
+@body({
+ "to": fields.Str(required=True),
+ "amount": fields.Str(required=True),
+ "txid": fields.Str(required=True),
+})
def post_contribution_confirmation(contribution_id, to, amount, txid):
contribution = ProposalContribution.query.filter_by(
id=contribution_id).first()
@@ -534,7 +515,7 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
if contribution.status == ContributionStatus.CONFIRMED:
# Duplicates can happen, just return ok
- return None, 200
+ return {"message": "ok"}, 200
# Convert to whole zcash coins from zats
zec_amount = str(from_zat(int(amount)))
@@ -581,14 +562,13 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
contribution.proposal.set_funded_when_ready()
db.session.commit()
- return None, 200
+ return {"message": "ok"}, 200
@blueprint.route("/contribution/", methods=["DELETE"])
@requires_auth
-@endpoint.api()
def delete_proposal_contribution(contribution_id):
- contribution = contribution = ProposalContribution.query.filter_by(
+ contribution = ProposalContribution.query.filter_by(
id=contribution_id).first()
if not contribution:
return {"message": "No contribution matching id"}, 404
@@ -602,13 +582,12 @@ def delete_proposal_contribution(contribution_id):
contribution.status = ContributionStatus.DELETED
db.session.add(contribution)
db.session.commit()
- return None, 202
+ return {"message": "ok"}, 202
# request MS payout
@blueprint.route("//milestone//request", methods=["PUT"])
@requires_team_member_auth
-@endpoint.api()
def request_milestone_payout(proposal_id, milestone_id):
if not g.current_proposal.is_funded:
return {"message": "Proposal is not fully funded"}, 400
@@ -630,7 +609,6 @@ def request_milestone_payout(proposal_id, milestone_id):
# accept MS payout (arbiter)
@blueprint.route("//milestone//accept", methods=["PUT"])
@requires_arbiter_auth
-@endpoint.api()
def accept_milestone_payout_request(proposal_id, milestone_id):
if not g.current_proposal.is_funded:
return {"message": "Proposal is not fully funded"}, 400
@@ -655,9 +633,9 @@ def accept_milestone_payout_request(proposal_id, milestone_id):
# reject MS payout (arbiter) (reason)
@blueprint.route("//milestone//reject", methods=["PUT"])
@requires_arbiter_auth
-@endpoint.api(
- parameter('reason', type=str, required=True),
-)
+@body({
+ "reason": fields.Str(required=True)
+})
def reject_milestone_payout_request(proposal_id, milestone_id, reason):
if not g.current_proposal.is_funded:
return {"message": "Proposal is not fully funded"}, 400
diff --git a/backend/grant/rfp/models.py b/backend/grant/rfp/models.py
index 4861ae50..fc12fa53 100644
--- a/backend/grant/rfp/models.py
+++ b/backend/grant/rfp/models.py
@@ -2,6 +2,7 @@ from datetime import datetime
from grant.extensions import ma, db
from grant.utils.enums import RFPStatus
from grant.utils.misc import dt_to_unix
+from grant.utils.enums import Category
class RFP(db.Model):
@@ -46,6 +47,8 @@ class RFP(db.Model):
matching: bool = False,
status: str = RFPStatus.DRAFT,
):
+ # TODO add status assert
+ assert Category.includes(category)
self.date_created = datetime.now()
self.title = title
self.brief = brief
diff --git a/backend/grant/rfp/views.py b/backend/grant/rfp/views.py
index 03c793b9..f60da4d8 100644
--- a/backend/grant/rfp/views.py
+++ b/backend/grant/rfp/views.py
@@ -1,5 +1,4 @@
-from flask import Blueprint, g
-from flask_yoloapi import endpoint, parameter
+from flask import Blueprint
from sqlalchemy import or_
from grant.utils.enums import RFPStatus
@@ -9,20 +8,18 @@ blueprint = Blueprint("rfp", __name__, url_prefix="/api/v1/rfps")
@blueprint.route("/", methods=["GET"])
-@endpoint.api()
def get_rfps():
rfps = RFP.query \
.filter(or_(
- RFP.status == RFPStatus.LIVE,
- RFP.status == RFPStatus.CLOSED,
- )) \
+ RFP.status == RFPStatus.LIVE,
+ RFP.status == RFPStatus.CLOSED,
+ )) \
.order_by(RFP.date_created.desc()) \
.all()
return rfps_schema.dump(rfps)
@blueprint.route("/", methods=["GET"])
-@endpoint.api()
def get_rfp(rfp_id):
rfp = RFP.query.filter_by(id=rfp_id).first()
if not rfp or rfp.status == RFPStatus.DRAFT:
diff --git a/backend/grant/user/views.py b/backend/grant/user/views.py
index 2fb9b28b..2ed025e1 100644
--- a/backend/grant/user/views.py
+++ b/backend/grant/user/views.py
@@ -1,8 +1,11 @@
from animal_case import keys_to_snake_case
from flask import Blueprint, g
-from flask_yoloapi import endpoint, parameter
+from marshmallow import fields
+
+import grant.utils.auth as auth
from grant.comment.models import Comment, user_comments_schema
from grant.email.models import EmailRecovery
+from grant.parser import query, body
from grant.proposal.models import (
Proposal,
proposal_team,
@@ -13,12 +16,10 @@ from grant.proposal.models import (
user_proposals_schema,
user_proposal_arbiters_schema
)
-import grant.utils.auth as auth
+from grant.utils.enums import ProposalStatus, ContributionStatus
from grant.utils.exceptions import ValidationException
from grant.utils.social import verify_social, get_social_login_url, VerifySocialException
from grant.utils.upload import remove_avatar, sign_avatar_upload, AvatarException
-from grant.utils.enums import ProposalStatus, ContributionStatus
-from flask import current_app
from .models import (
User,
SocialMedia,
@@ -34,9 +35,9 @@ blueprint = Blueprint('user', __name__, url_prefix='/api/v1/users')
@blueprint.route("/", methods=["GET"])
-@endpoint.api(
- parameter('proposalId', type=str, required=False)
-)
+@query({
+ "proposalId": fields.Str(required=False, missing=None)
+})
def get_users(proposal_id):
proposal = Proposal.query.filter_by(id=proposal_id).first()
if not proposal:
@@ -44,10 +45,10 @@ def get_users(proposal_id):
else:
users = (
User.query
- .join(proposal_team)
- .join(Proposal)
- .filter(proposal_team.c.proposal_id == proposal.id)
- .all()
+ .join(proposal_team)
+ .join(Proposal)
+ .filter(proposal_team.c.proposal_id == proposal.id)
+ .all()
)
result = users_schema.dump(users)
return result
@@ -55,20 +56,19 @@ def get_users(proposal_id):
@blueprint.route("/me", methods=["GET"])
@auth.requires_auth
-@endpoint.api()
def get_me():
dumped_user = self_user_schema.dump(g.current_user)
return dumped_user
@blueprint.route("/", methods=["GET"])
-@endpoint.api(
- parameter("withProposals", type=bool, required=False),
- parameter("withComments", type=bool, required=False),
- parameter("withFunded", type=bool, required=False),
- parameter("withPending", type=bool, required=False),
- parameter("withArbitrated", type=bool, required=False)
-)
+@query({
+ "withProposals": fields.Bool(required=False, missing=None),
+ "withComments": fields.Bool(required=False, missing=None),
+ "withFunded": fields.Bool(required=False, missing=None),
+ "withPending": fields.Bool(required=False, missing=None),
+ "withArbitrated": fields.Bool(required=False, missing=None)
+})
def get_user(user_id, with_proposals, with_comments, with_funded, with_pending, with_arbitrated):
user = User.get_by_id(user_id)
if user:
@@ -109,12 +109,13 @@ def get_user(user_id, with_proposals, with_comments, with_funded, with_pending,
@blueprint.route("/", methods=["POST"])
-@endpoint.api(
- parameter('emailAddress', type=str, required=True),
- parameter('password', type=str, required=True),
- parameter('displayName', type=str, required=True),
- parameter('title', type=str, required=True)
-)
+@body({
+ # TODO guard all (valid, minimum, maximum)
+ "emailAddress": fields.Str(required=True),
+ "password": fields.Str(required=True),
+ "displayName": fields.Str(required=True),
+ "title": fields.Str(required=True),
+})
def create_user(
email_address,
password,
@@ -137,10 +138,10 @@ def create_user(
@blueprint.route("/auth", methods=["POST"])
-@endpoint.api(
- parameter('email', type=str, required=True),
- parameter('password', type=str, required=True)
-)
+@body({
+ "email": fields.Str(required=True),
+ "password": fields.Str(required=True)
+})
def auth_user(email, password):
authed_user = auth.auth_user(email, password)
return self_user_schema.dump(authed_user)
@@ -148,49 +149,48 @@ def auth_user(email, password):
@blueprint.route("/me/password", methods=["PUT"])
@auth.requires_auth
-@endpoint.api(
- parameter('currentPassword', type=str, required=True),
- parameter('password', type=str, required=True),
-)
+# TODO gaurd password (minimum)
+@body({
+ "currentPassword": fields.Str(required=True),
+ "password": fields.Str(required=True)
+})
def update_user_password(current_password, password):
if not g.current_user.check_password(current_password):
return {"message": "Current password incorrect"}, 403
g.current_user.set_password(password)
- return None, 200
+ return {"message": "ok"}, 200
@blueprint.route("/me/email", methods=["PUT"])
@auth.requires_auth
-@endpoint.api(
- parameter('email', type=str, required=True),
- parameter('password', type=str, required=True)
-)
+# TODO gaurd all (valid, minimum)
+@body({
+ "email": fields.Str(required=True),
+ "password": fields.Str(required=True)
+})
def update_user_email(email, password):
if not g.current_user.check_password(password):
return {"message": "Password is incorrect"}, 403
g.current_user.set_email(email)
- return None, 200
+ return {"message": "ok"}, 200
@blueprint.route("/me/resend-verification", methods=["PUT"])
@auth.requires_auth
-@endpoint.api()
def resend_email_verification():
g.current_user.send_verification_email()
- return None, 200
+ return {"message": "ok"}, 200
@blueprint.route("/logout", methods=["POST"])
@auth.requires_auth
-@endpoint.api()
def logout_user():
auth.logout_current_user()
- return None, 200
+ return {"message": "ok"}, 200
@blueprint.route("/social//authurl", methods=["GET"])
@auth.requires_auth
-@endpoint.api()
def get_user_social_auth_url(service):
try:
return {"url": get_social_login_url(service)}
@@ -201,9 +201,9 @@ def get_user_social_auth_url(service):
@blueprint.route("/social//verify", methods=["POST"])
@auth.requires_auth
-@endpoint.api(
- parameter('code', type=str, required=True)
-)
+@body({
+ "code": fields.Str(required=True)
+})
def verify_user_social(service, code):
try:
# 1. verify with 3rd party
@@ -227,22 +227,23 @@ def verify_user_social(service, code):
@blueprint.route("/recover", methods=["POST"])
-@endpoint.api(
- parameter('email', type=str, required=True)
-)
+@body({
+ "email": fields.Str(required=True)
+})
def recover_user(email):
existing_user = User.get_by_email(email)
if not existing_user:
return {"message": "No user exists with that email"}, 400
auth.throw_on_banned(existing_user)
existing_user.send_recovery_email()
- return None, 200
+ return {"message": "ok"}, 200
@blueprint.route("/recover/", methods=["POST"])
-@endpoint.api(
- parameter('password', type=str, required=True),
-)
+# TODO gaurd length
+@body({
+ "password": fields.Str(required=True)
+})
def recover_email(code, password):
er = EmailRecovery.query.filter_by(code=code).first()
if er:
@@ -252,16 +253,16 @@ def recover_email(code, password):
er.user.set_password(password)
db.session.delete(er)
db.session.commit()
- return None, 200
+ return {"message": "ok"}, 200
return {"message": "Invalid reset code"}, 400
@blueprint.route("/avatar", methods=["POST"])
@auth.requires_auth
-@endpoint.api(
- parameter('mimetype', type=str, required=True)
-)
+@body({
+ "mimetype": fields.Str(required=True)
+})
def upload_avatar(mimetype):
user = g.current_user
try:
@@ -273,9 +274,9 @@ def upload_avatar(mimetype):
@blueprint.route("/avatar", methods=["DELETE"])
@auth.requires_auth
-@endpoint.api(
- parameter('url', type=str, required=True)
-)
+@body({
+ "url": fields.Str(required=True)
+})
def delete_avatar(url):
user = g.current_user
remove_avatar(url, user.id)
@@ -284,12 +285,13 @@ def delete_avatar(url):
@blueprint.route("/", methods=["PUT"])
@auth.requires_auth
@auth.requires_same_user_auth
-@endpoint.api(
- parameter('displayName', type=str, required=True),
- parameter('title', type=str, required=True),
- parameter('socialMedias', type=list, required=True),
- parameter('avatar', type=str, required=True)
-)
+# TODO gaurd all (minimum, minimum, shape, uri)
+@body({
+ "displayName": fields.Str(required=True),
+ "title": fields.Str(required=True),
+ "socialMedias": fields.List(fields.Dict(), required=True),
+ "avatar": fields.Str(required=True)
+})
def update_user(user_id, display_name, title, social_medias, avatar):
user = g.current_user
@@ -324,7 +326,6 @@ def update_user(user_id, display_name, title, social_medias, avatar):
@blueprint.route("//invites", methods=["GET"])
@auth.requires_same_user_auth
-@endpoint.api()
def get_user_invites(user_id):
invites = ProposalTeamInvite.get_pending_for_user(g.current_user)
return invites_with_proposal_schema.dump(invites)
@@ -332,9 +333,9 @@ def get_user_invites(user_id):
@blueprint.route("//invites//respond", methods=["PUT"])
@auth.requires_same_user_auth
-@endpoint.api(
- parameter('response', type=bool, required=True)
-)
+@body({
+ "response": fields.Bool(required=True)
+})
def respond_to_invite(user_id, invite_id, response):
invite = ProposalTeamInvite.query.filter_by(id=invite_id).first()
if not invite:
@@ -348,22 +349,22 @@ def respond_to_invite(user_id, invite_id, response):
db.session.add(invite)
db.session.commit()
- return None, 200
+ return {"message": "ok"}, 200
@blueprint.route("//settings", methods=["GET"])
@auth.requires_same_user_auth
-@endpoint.api()
def get_user_settings(user_id):
return user_settings_schema.dump(g.current_user.settings)
@blueprint.route("//settings", methods=["PUT"])
@auth.requires_same_user_auth
-@endpoint.api(
- parameter('emailSubscriptions', type=dict),
- parameter('refundAddress', type=str)
-)
+# TODO guard all (shape, validity)
+@body({
+ "emailSubscriptions": fields.Dict(required=True),
+ "refundAddress": fields.Str(required=False, missing=None)
+})
def set_user_settings(user_id, email_subscriptions, refund_address):
if email_subscriptions:
try:
@@ -381,9 +382,9 @@ def set_user_settings(user_id, email_subscriptions, refund_address):
@blueprint.route("//arbiter/", methods=["PUT"])
@auth.requires_same_user_auth
-@endpoint.api(
- parameter('isAccept', type=bool)
-)
+@body({
+ "isAccept": fields.Bool(required=False, missing=None)
+})
def set_user_arbiter(user_id, proposal_id, is_accept):
try:
proposal = Proposal.query.filter_by(id=int(proposal_id)).first()
@@ -399,5 +400,3 @@ def set_user_arbiter(user_id, proposal_id, is_accept):
except ValidationException as e:
return {"message": str(e)}, 400
-
- return user_settings_schema.dump(g.current_user.settings)
diff --git a/backend/requirements/prod.txt b/backend/requirements/prod.txt
index fdf93c60..ccbde94f 100644
--- a/backend/requirements/prod.txt
+++ b/backend/requirements/prod.txt
@@ -53,9 +53,6 @@ markdownify
# email
sendgrid==5.6.0
-# input validation
-flask-yolo2API==0.2.6
-
#sentry
sentry-sdk[flask]==0.5.5
@@ -71,5 +68,11 @@ Flask-Security==3.0.0
# oauth
requests-oauthlib==1.0.0
+# request parsing
+webargs==5.1.2
+
# 2fa - totp
pyotp==2.2.7
+
+# JSON formatting
+animal_case==0.4.1
\ No newline at end of file
diff --git a/backend/tests/admin/test_api.py b/backend/tests/admin/test_admin_api.py
similarity index 86%
rename from backend/tests/admin/test_api.py
rename to backend/tests/admin/test_admin_api.py
index 418d3bc5..7360a51a 100644
--- a/backend/tests/admin/test_api.py
+++ b/backend/tests/admin/test_admin_api.py
@@ -72,9 +72,9 @@ class TestAdminAPI(BaseProposalCreatorConfig):
def assert_autherror(self, resp, contains):
# this should be 403
- self.assert500(resp)
- print(f'...check that [{resp.json["data"]}] contains [{contains}]')
- self.assertTrue(contains in resp.json['data'])
+ self.assert403(resp)
+ print(f'...check that [{resp.json["message"]}] contains [{contains}]')
+ self.assertTrue(contains in resp.json['message'])
# happy path (mostly)
def test_admin_2fa_setup_flow(self):
@@ -245,22 +245,22 @@ class TestAdminAPI(BaseProposalCreatorConfig):
def test_update_proposal(self):
self.login_admin()
# set to 1 (on)
- resp_on = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data={"contributionMatching": 1})
+ resp_on = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data=json.dumps({"contributionMatching": 1}))
self.assert200(resp_on)
self.assertEqual(resp_on.json['contributionMatching'], 1)
- resp_off = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data={"contributionMatching": 0})
+ resp_off = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data=json.dumps({"contributionMatching": 0}))
self.assert200(resp_off)
self.assertEqual(resp_off.json['contributionMatching'], 0)
def test_update_proposal_no_auth(self):
- resp = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data={"contributionMatching": 1})
+ resp = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data=json.dumps({"contributionMatching": 1}))
self.assert401(resp)
def test_update_proposal_bad_matching(self):
self.login_admin()
- resp = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data={"contributionMatching": 2})
- self.assert500(resp)
- self.assertIn('Bad value', resp.json['data'])
+ resp = self.app.put(f"/api/v1/admin/proposals/{self.proposal.id}", data=json.dumps({"contributionMatching": 2}))
+ self.assert400(resp)
+ self.assertTrue(resp.json['message'])
@patch('requests.get', side_effect=mock_blockchain_api_requests)
def test_approve_proposal(self, mock_get):
@@ -272,7 +272,7 @@ class TestAdminAPI(BaseProposalCreatorConfig):
# approve
resp = self.app.put(
"/api/v1/admin/proposals/{}/approve".format(self.proposal.id),
- data={"isApprove": True}
+ data=json.dumps({"isApprove": True})
)
self.assert200(resp)
self.assertEqual(resp.json["status"], ProposalStatus.APPROVED)
@@ -287,7 +287,7 @@ class TestAdminAPI(BaseProposalCreatorConfig):
# reject
resp = self.app.put(
"/api/v1/admin/proposals/{}/approve".format(self.proposal.id),
- data={"isApprove": False, "rejectReason": "Funnzies."}
+ data=json.dumps({"isApprove": False, "rejectReason": "Funnzies."})
)
self.assert200(resp)
self.assertEqual(resp.json["status"], ProposalStatus.REJECTED)
@@ -301,10 +301,43 @@ class TestAdminAPI(BaseProposalCreatorConfig):
# nominate arbiter
resp = self.app.put(
"/api/v1/admin/arbiters",
- data={
+ data=json.dumps({
'proposalId': self.proposal.id,
'userId': self.other_user.id
- }
+ })
)
self.assert200(resp)
# TODO - more tests
+
+ def test_create_rfp_succeeds(self):
+ self.login_admin()
+
+ resp = self.app.post(
+ "/api/v1/admin/rfps",
+ data=json.dumps({
+ "brief": "Some brief",
+ "category": "CORE_DEV",
+ "content": "CONTENT",
+ "dateCloses": 1553980004,
+ "status": "DRAFT",
+ "title": "TITLE"
+ })
+ )
+ self.assert200(resp)
+
+ def test_create_rfp_fails_with_bad_category(self):
+ self.login_admin()
+
+ resp = self.app.post(
+ "/api/v1/admin/rfps",
+ data=json.dumps({
+ "brief": "Some brief",
+ "category": "NOT_CORE_DEV",
+ "content": "CONTENT",
+ "dateCloses": 1553980004,
+ "status": "DRAFT",
+ "title": "TITLE"
+ })
+ )
+ self.assert400(resp)
+
diff --git a/backend/tests/proposal/test_api.py b/backend/tests/proposal/test_api.py
index 3adc28bc..d399e369 100644
--- a/backend/tests/proposal/test_api.py
+++ b/backend/tests/proposal/test_api.py
@@ -45,7 +45,7 @@ class TestProposalAPI(BaseProposalCreatorConfig):
data=json.dumps(new_proposal),
content_type='application/json'
)
-
+ print(resp)
self.assert200(resp)
self.assertEqual(resp.json["title"], new_title)
self.assertEqual(self.proposal.title, new_title)
diff --git a/backend/tests/user/test_user_api.py b/backend/tests/user/test_user_api.py
index 6e67478c..a8456a57 100644
--- a/backend/tests/user/test_user_api.py
+++ b/backend/tests/user/test_user_api.py
@@ -104,10 +104,10 @@ class TestUserAPI(BaseUserConfig):
}),
content_type="application/json"
)
- # self.assert403(user_auth_resp)
- # self.assertTrue(user_auth_resp.json['message'] is not None)
- self.assert500(user_auth_resp)
- self.assertIn('Invalid pass', user_auth_resp.json['data'])
+ self.assert403(user_auth_resp)
+ self.assertTrue(user_auth_resp.json['message'] is not None)
+ # self.assert500(user_auth_resp)
+ # self.assertIn('Invalid pass', user_auth_resp.json['data'])
def test_user_auth_bad_email(self):
user_auth_resp = self.app.post(
@@ -118,10 +118,10 @@ class TestUserAPI(BaseUserConfig):
}),
content_type="application/json"
)
- # self.assert400(user_auth_resp)
- # self.assertTrue(user_auth_resp.json['message'] is not None)
- self.assert500(user_auth_resp)
- self.assertIn('No user', user_auth_resp.json['data'])
+ self.assert403(user_auth_resp)
+ self.assertTrue(user_auth_resp.json['message'] is not None)
+ # self.assert500(user_auth_resp)
+ # self.assertIn('No user', user_auth_resp.json['data'])
def test_user_auth_banned(self):
self.user.set_banned(True, 'reason for banning')
@@ -134,8 +134,8 @@ class TestUserAPI(BaseUserConfig):
content_type="application/json"
)
# in test mode we get 500s instead of 403
- self.assert500(user_auth_resp)
- self.assertIn('banned', user_auth_resp.json['data'])
+ self.assert403(user_auth_resp)
+ self.assertIn('banned', user_auth_resp.json['message'])
def test_create_user_duplicate_400(self):
# self.user is identical to test_user, should throw
@@ -152,7 +152,7 @@ class TestUserAPI(BaseUserConfig):
self.login_default_user()
updated_user = animalify(copy.deepcopy(user_schema.dump(self.user)))
updated_user["displayName"] = 'new display name'
- updated_user["avatar"] = {}
+ updated_user["avatar"] = '' # TODO confirm avatar is no longer a dict
updated_user["socialMedias"] = []
user_update_resp = self.app.put(
@@ -253,8 +253,9 @@ class TestUserAPI(BaseUserConfig):
content_type='application/json'
)
# 404 outside testing mode
- self.assertStatus(response, 500)
- self.assertIn('banned', response.json['data'])
+ self.assertStatus(response, 403)
+ print(response.json)
+ self.assertIn('banned', response.json['message'])
def test_recover_user_no_user(self):
response = self.app.post(
@@ -301,8 +302,8 @@ class TestUserAPI(BaseUserConfig):
content_type='application/json'
)
# 403 outside of testing mode
- self.assertStatus(reset_resp, 500)
- self.assertIn('banned', reset_resp.json['data'])
+ self.assertStatus(reset_resp, 403)
+ self.assertIn('banned', reset_resp.json['message'])
@patch('grant.user.views.verify_social')
def test_user_verify_social(self, mock_verify_social):
From 2cc5ade6730af2565763958f122ad6af50eebb64 Mon Sep 17 00:00:00 2001
From: Will O'Beirne
Date: Mon, 4 Mar 2019 13:52:57 -0500
Subject: [PATCH 06/72] Sort homepage rfps by bounty. Fix admin views and some
bounty logic.
---
backend/grant/admin/views.py | 9 +++++----
backend/grant/app.py | 1 +
backend/grant/proposal/models.py | 2 +-
frontend/client/components/Home/Requests.tsx | 12 +++++++++++-
4 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/backend/grant/admin/views.py b/backend/grant/admin/views.py
index 21e775a2..0a29ae68 100644
--- a/backend/grant/admin/views.py
+++ b/backend/grant/admin/views.py
@@ -496,9 +496,10 @@ def get_rfp(rfp_id):
"brief": fields.Str(required=True),
"content": fields.Str(required=True),
"category": fields.Str(required=True, validate=validate.OneOf(choices=Category.list())),
- "bounty": fields.Str(required=True),
- "matching": fields.Bool(required=True, default=False, missing=False),
- "dateCloses": fields.Int(required=True)
+ "bounty": fields.Str(required=False, missing=""),
+ "matching": fields.Bool(required=False, default=False, missing=False),
+ "dateCloses": fields.Int(required=False, missing=None),
+ "status": fields.Str(required=True, validate=validate.OneOf(choices=RFPStatus.list())),
})
@admin.admin_auth_required
def update_rfp(rfp_id, title, brief, content, category, bounty, matching, date_closes, status):
@@ -511,8 +512,8 @@ def update_rfp(rfp_id, title, brief, content, category, bounty, matching, date_c
rfp.brief = brief
rfp.content = content
rfp.category = category
- rfp.bounty = bounty
rfp.matching = matching
+ rfp.bounty = bounty if bounty and bounty != "" else None
rfp.date_closes = datetime.fromtimestamp(date_closes) if date_closes else None
# Update timestamps if status changed
diff --git a/backend/grant/app.py b/backend/grant/app.py
index 6c1953ec..8724540f 100644
--- a/backend/grant/app.py
+++ b/backend/grant/app.py
@@ -38,6 +38,7 @@ def create_app(config_objects=["grant.settings"]):
@app.errorhandler(422)
@app.errorhandler(400)
def handle_error(err):
+ print(err)
headers = err.data.get("headers", None)
messages = err.data.get("messages", "Invalid request.")
error_message = "Something went wrong with your request. That's all we know"
diff --git a/backend/grant/proposal/models.py b/backend/grant/proposal/models.py
index 518d7a98..5464ec4c 100644
--- a/backend/grant/proposal/models.py
+++ b/backend/grant/proposal/models.py
@@ -506,7 +506,7 @@ class Proposal(db.Model):
# apply matching multiplier
funded = Decimal(self.contributed) * Decimal(1 + self.contribution_matching)
# apply bounty, if available
- if self.rfp:
+ if self.rfp and self.rfp.bounty and self.rfp.bounty != "":
funded = funded + Decimal(self.rfp.bounty)
# if funded > target, just set as target
if funded > target:
diff --git a/frontend/client/components/Home/Requests.tsx b/frontend/client/components/Home/Requests.tsx
index 94cb01c6..d99bca24 100644
--- a/frontend/client/components/Home/Requests.tsx
+++ b/frontend/client/components/Home/Requests.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import BN from 'bn.js';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { withNamespaces, WithNamespaces } from 'react-i18next';
@@ -28,7 +29,16 @@ class HomeRequests extends React.Component {
render() {
const { t, rfps, isFetchingRfps } = this.props;
- const activeRfps = (rfps || []).filter(rfp => rfp.status === RFP_STATUS.LIVE).slice(0, 2);
+
+ // 2 live RFPs, sorted by highest bounty first
+ const activeRfps = (rfps || [])
+ .filter(rfp => rfp.status === RFP_STATUS.LIVE)
+ .sort((a, b) => {
+ const aBounty = a.bounty || new BN(0);
+ const bBounty = b.bounty || new BN(0);
+ return bBounty.sub(aBounty).toNumber();
+ })
+ .slice(0, 2);
let content;
if (activeRfps.length) {
From 58ee787ab8d0f0d099ee01b1b6ff2a977a3459c3 Mon Sep 17 00:00:00 2001
From: Will O'Beirne
Date: Mon, 4 Mar 2019 13:56:57 -0500
Subject: [PATCH 07/72] Remove print.
---
backend/grant/app.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/backend/grant/app.py b/backend/grant/app.py
index 8724540f..6c1953ec 100644
--- a/backend/grant/app.py
+++ b/backend/grant/app.py
@@ -38,7 +38,6 @@ def create_app(config_objects=["grant.settings"]):
@app.errorhandler(422)
@app.errorhandler(400)
def handle_error(err):
- print(err)
headers = err.data.get("headers", None)
messages = err.data.get("messages", "Invalid request.")
error_message = "Something went wrong with your request. That's all we know"
From d5e059a474b08c7fe4a323e0fd63acac5b2b2e6b Mon Sep 17 00:00:00 2001
From: Will O'Beirne
Date: Mon, 4 Mar 2019 14:02:07 -0500
Subject: [PATCH 08/72] Close drawer on navigation.
---
frontend/client/components/Header/Drawer.tsx | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/frontend/client/components/Header/Drawer.tsx b/frontend/client/components/Header/Drawer.tsx
index 32d41cdc..3445d037 100644
--- a/frontend/client/components/Header/Drawer.tsx
+++ b/frontend/client/components/Header/Drawer.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { Drawer, Menu } from 'antd';
+import { withRouter, RouteComponentProps } from 'react-router';
import { Link } from 'react-router-dom';
import UserAvatar from 'components/UserAvatar';
import { AppState } from 'store/reducers';
@@ -15,7 +16,7 @@ interface OwnProps {
onClose(): void;
}
-type Props = StateProps & OwnProps;
+type Props = StateProps & OwnProps & RouteComponentProps;
class HeaderDrawer extends React.Component {
componentDidMount() {
@@ -26,6 +27,12 @@ class HeaderDrawer extends React.Component {
window.removeEventListener('resize', this.props.onClose);
}
+ componentDidUpdate(prevProps: Props) {
+ if (this.props.location.pathname !== prevProps.location.pathname) {
+ this.props.onClose();
+ }
+ }
+
render() {
const { isOpen, onClose, user } = this.props;
@@ -82,4 +89,4 @@ class HeaderDrawer extends React.Component {
export default connect(state => ({
user: state.auth.user,
-}))(HeaderDrawer);
+}))(withRouter(HeaderDrawer));
From 831488d54d5cbdce40c4b0ebf42e9b9056ca73bc Mon Sep 17 00:00:00 2001
From: Will O'Beirne
Date: Mon, 4 Mar 2019 14:09:37 -0500
Subject: [PATCH 09/72] Highlight active page correctly.
---
frontend/client/components/Header/Drawer.tsx | 27 ++++++++++++--------
1 file changed, 16 insertions(+), 11 deletions(-)
diff --git a/frontend/client/components/Header/Drawer.tsx b/frontend/client/components/Header/Drawer.tsx
index 3445d037..f74e5e9f 100644
--- a/frontend/client/components/Header/Drawer.tsx
+++ b/frontend/client/components/Header/Drawer.tsx
@@ -34,7 +34,7 @@ class HeaderDrawer extends React.Component {
}
render() {
- const { isOpen, onClose, user } = this.props;
+ const { isOpen, onClose, user, location } = this.props;
let userTitle: React.ReactNode = 'Account';
if (user) {
@@ -53,31 +53,36 @@ class HeaderDrawer extends React.Component {
placement="left"
>
Navigation
-