Address TODOs (#349)
* todos: simple ones, removals & modifications to NOTE * rem-todo: reduced markdown images are removed by sanitizer * be todo: add user validation to create * be todo: improve test_invide_api tests * be todo: remove todo comment * fe todo: set error messages on reducers * fe todo: upgrade and enable react-helmet * todos - remove uneeded * fe todos: remove unecessary * be: fix remaining staking contribution calculation
This commit is contained in:
parent
25a71ced1b
commit
1ae519e251
|
@ -401,7 +401,6 @@ class ProposalDetailNaked extends React.Component<Props, State> {
|
|||
<Markdown source={p.content} />
|
||||
</Collapse.Panel>
|
||||
|
||||
{/* TODO - comments, milestones, updates &etc. */}
|
||||
<Collapse.Panel key="json" header="json">
|
||||
<pre>{JSON.stringify(p, null, 4)}</pre>
|
||||
</Collapse.Panel>
|
||||
|
@ -469,8 +468,6 @@ class ProposalDetailNaked extends React.Component<Props, State> {
|
|||
</div>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
{/* TODO: contributors here? */}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
|
|
@ -22,7 +22,6 @@ class Milestone(db.Model):
|
|||
content = db.Column(db.Text, nullable=False)
|
||||
payout_percent = db.Column(db.String(255), nullable=False)
|
||||
immediate_payout = db.Column(db.Boolean)
|
||||
# TODO: change to estimated_duration (sec or ms) -- FE can calc from dates on draft
|
||||
date_estimated = db.Column(db.DateTime, nullable=False)
|
||||
|
||||
stage = db.Column(db.String(255), nullable=False)
|
||||
|
|
|
@ -376,14 +376,14 @@ class Proposal(db.Model):
|
|||
|
||||
def get_staking_contribution(self, user_id: int):
|
||||
contribution = None
|
||||
remaining = PROPOSAL_STAKING_AMOUNT - Decimal(self.contributed)
|
||||
remaining = PROPOSAL_STAKING_AMOUNT - Decimal(self.amount_staked)
|
||||
# check funding
|
||||
if remaining > 0:
|
||||
# find pending contribution for any user of remaining amount
|
||||
# TODO: Filter by staking=True?
|
||||
contribution = ProposalContribution.query.filter_by(
|
||||
proposal_id=self.id,
|
||||
status=ProposalStatus.PENDING,
|
||||
staking=True,
|
||||
).first()
|
||||
if not contribution:
|
||||
contribution = self.create_contribution(
|
||||
|
@ -537,6 +537,14 @@ class Proposal(db.Model):
|
|||
funded = reduce(lambda prev, c: prev + Decimal(c.amount), contributions, 0)
|
||||
return str(funded)
|
||||
|
||||
@hybrid_property
|
||||
def amount_staked(self):
|
||||
contributions = ProposalContribution.query \
|
||||
.filter_by(proposal_id=self.id, status=ContributionStatus.CONFIRMED, staking=True) \
|
||||
.all()
|
||||
amount = reduce(lambda prev, c: prev + Decimal(c.amount), contributions, 0)
|
||||
return str(amount)
|
||||
|
||||
@hybrid_property
|
||||
def funded(self):
|
||||
target = Decimal(self.target)
|
||||
|
@ -716,9 +724,6 @@ proposal_team_invite_schema = ProposalTeamInviteSchema()
|
|||
proposal_team_invites_schema = ProposalTeamInviteSchema(many=True)
|
||||
|
||||
|
||||
# TODO: Find a way to extend ProposalTeamInviteSchema instead of redefining
|
||||
|
||||
|
||||
class InviteWithProposalSchema(ma.Schema):
|
||||
class Meta:
|
||||
model = ProposalTeamInvite
|
||||
|
@ -768,7 +773,7 @@ class ProposalContributionSchema(ma.Schema):
|
|||
|
||||
def get_addresses(self, obj):
|
||||
# Omit 'memo' and 'sprout' for now
|
||||
# TODO: Add back in 'sapling' when ready
|
||||
# NOTE: Add back in 'sapling' when ready
|
||||
addresses = blockchain_get('/contribution/addresses', {'contributionId': obj.id})
|
||||
return {
|
||||
'transparent': addresses['transparent'],
|
||||
|
|
|
@ -4,6 +4,7 @@ from decimal import Decimal
|
|||
from flask import Blueprint, g, request, current_app
|
||||
from marshmallow import fields, validate
|
||||
from sqlalchemy import or_
|
||||
from sentry_sdk import capture_message
|
||||
|
||||
from grant.extensions import limiter
|
||||
from grant.comment.models import Comment, comment_schema, comments_schema
|
||||
|
@ -136,7 +137,7 @@ def post_proposal_comments(proposal_id, comment, parent_comment_id):
|
|||
db.session.commit()
|
||||
dumped_comment = comment_schema.dump(comment)
|
||||
|
||||
# TODO: Email proposal team if top-level comment
|
||||
# Email proposal team if top-level comment
|
||||
if not parent:
|
||||
for member in proposal.team:
|
||||
send_email(member.email_address, 'proposal_comment', {
|
||||
|
@ -407,7 +408,6 @@ def post_proposal_team_invite(proposal_id, address):
|
|||
db.session.commit()
|
||||
|
||||
# Send email
|
||||
# TODO: Move this to some background task / after request action
|
||||
email = address
|
||||
user = User.get_by_email(email_address=address)
|
||||
if user:
|
||||
|
@ -531,8 +531,9 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
|
|||
id=contribution_id).first()
|
||||
|
||||
if not contribution:
|
||||
# TODO: Log in sentry
|
||||
current_app.logger.warn(f'Unknown contribution {contribution_id} confirmed with txid {txid}')
|
||||
msg = f'Unknown contribution {contribution_id} confirmed with txid {txid}, amount {amount}'
|
||||
capture_message(msg)
|
||||
current_app.logger.warn(msg)
|
||||
return {"message": "No contribution matching id"}, 404
|
||||
|
||||
if contribution.status == ContributionStatus.CONFIRMED:
|
||||
|
@ -578,8 +579,6 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
|
|||
'contributor_url': make_url(f'/profile/{contribution.user.id}') if contribution.user else '',
|
||||
})
|
||||
|
||||
# TODO: Once we have a task queuer in place, queue emails to everyone
|
||||
|
||||
# on funding target reached.
|
||||
contribution.proposal.set_funded_when_ready()
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ class RFP(db.Model):
|
|||
matching: bool = False,
|
||||
status: str = RFPStatus.DRAFT,
|
||||
):
|
||||
# TODO add status assert
|
||||
assert RFPStatus.includes(status)
|
||||
assert Category.includes(category)
|
||||
self.id = gen_random_id(RFP)
|
||||
self.date_created = datetime.now()
|
||||
|
|
|
@ -30,7 +30,6 @@ class ProposalReminder:
|
|||
assert task.job_type == 1, "Job type: {} is incorrect for ProposalReminder".format(task.job_type)
|
||||
from grant.proposal.models import Proposal
|
||||
proposal = Proposal.query.filter_by(id=task.blob["proposal_id"]).first()
|
||||
# TODO - replace with email
|
||||
print(proposal)
|
||||
task.completed = True
|
||||
db.session.add(task)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from flask_security import UserMixin, RoleMixin
|
||||
from flask_security.core import current_user
|
||||
from flask_security.utils import hash_password, verify_and_update_password, login_user
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from grant.comment.models import Comment
|
||||
from grant.email.models import EmailVerification, EmailRecovery
|
||||
from grant.email.send import send_email
|
||||
|
@ -10,11 +11,11 @@ from grant.email.subscription_settings import (
|
|||
email_subscriptions_to_dict
|
||||
)
|
||||
from grant.extensions import ma, db, security
|
||||
from grant.utils.misc import make_url, gen_random_id
|
||||
from grant.utils.misc import make_url, gen_random_id, is_email
|
||||
from grant.utils.social import generate_social_url
|
||||
from grant.utils.upload import extract_avatar_filename, construct_avatar_url
|
||||
from grant.utils import totp_2fa
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from grant.utils.exceptions import ValidationException
|
||||
|
||||
|
||||
def is_current_authed_user_id(user_id):
|
||||
|
@ -133,8 +134,6 @@ class User(db.Model, UserMixin):
|
|||
backref=db.backref('users', lazy='dynamic'))
|
||||
arbiter_proposals = db.relationship("ProposalArbiter", lazy=True, back_populates="user")
|
||||
|
||||
# TODO - add create and validate methods
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
email_address,
|
||||
|
@ -150,6 +149,22 @@ class User(db.Model, UserMixin):
|
|||
self.title = title
|
||||
self.password = password
|
||||
|
||||
@staticmethod
|
||||
def validate(user):
|
||||
em = user.get('email_address')
|
||||
if not em:
|
||||
raise ValidationException('Must have email address')
|
||||
if not is_email(em):
|
||||
raise ValidationException('Email address looks invalid')
|
||||
|
||||
t = user.get('title')
|
||||
if t and len(t) > 255:
|
||||
raise ValidationException('Title is too long')
|
||||
|
||||
dn = user.get('display_name')
|
||||
if dn and len(dn) > 255:
|
||||
raise ValidationException('Display name is too long')
|
||||
|
||||
@staticmethod
|
||||
def create(email_address=None, password=None, display_name=None, title=None, _send_email=True):
|
||||
user = security.datastore.create_user(
|
||||
|
@ -158,6 +173,7 @@ class User(db.Model, UserMixin):
|
|||
display_name=display_name,
|
||||
title=title
|
||||
)
|
||||
User.validate(vars(user))
|
||||
security.datastore.commit()
|
||||
|
||||
# user settings
|
||||
|
@ -249,7 +265,6 @@ class User(db.Model, UserMixin):
|
|||
db.session.flush()
|
||||
|
||||
def set_admin(self, is_admin: bool):
|
||||
# TODO: audit entry & possibly email user
|
||||
self.is_admin = is_admin
|
||||
db.session.add(self)
|
||||
db.session.flush()
|
||||
|
|
|
@ -245,10 +245,12 @@ 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=json.dumps({"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=json.dumps({"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)
|
||||
|
||||
|
@ -307,7 +309,6 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
|||
})
|
||||
)
|
||||
self.assert200(resp)
|
||||
# TODO - more tests
|
||||
|
||||
def test_create_rfp_succeeds(self):
|
||||
self.login_admin()
|
||||
|
@ -315,12 +316,12 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
|||
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"
|
||||
"brief": "Some brief",
|
||||
"category": "CORE_DEV",
|
||||
"content": "CONTENT",
|
||||
"dateCloses": 1553980004,
|
||||
"status": "DRAFT",
|
||||
"title": "TITLE"
|
||||
})
|
||||
)
|
||||
self.assert200(resp)
|
||||
|
@ -331,13 +332,12 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
|||
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"
|
||||
"brief": "Some brief",
|
||||
"category": "NOT_CORE_DEV",
|
||||
"content": "CONTENT",
|
||||
"dateCloses": 1553980004,
|
||||
"status": "DRAFT",
|
||||
"title": "TITLE"
|
||||
})
|
||||
)
|
||||
self.assert400(resp)
|
||||
|
||||
|
|
|
@ -47,7 +47,10 @@ class TestUserInviteAPI(BaseProposalCreatorConfig):
|
|||
self.assertStatus(invites_res, 200)
|
||||
|
||||
# Make sure we made the team, coach
|
||||
self.assertTrue(len(self.other_proposal.team) == 2) # TODO: More thorough check than length
|
||||
print(self.other_proposal.team)
|
||||
self.assertTrue(len(self.other_proposal.team) == 2)
|
||||
team_ids = [t.id for t in self.other_proposal.team]
|
||||
self.assertIn(self.user.id, team_ids, 'user should be in team')
|
||||
|
||||
def test_put_user_invite_response_reject(self):
|
||||
invite = ProposalTeamInvite(
|
||||
|
@ -67,7 +70,9 @@ class TestUserInviteAPI(BaseProposalCreatorConfig):
|
|||
self.assertStatus(invites_res, 200)
|
||||
|
||||
# Make sure we made the team, coach
|
||||
self.assertTrue(len(self.other_proposal.team) == 1) # TODO: More thorough check than length
|
||||
self.assertTrue(len(self.other_proposal.team) == 1)
|
||||
team_ids = [t.id for t in self.other_proposal.team]
|
||||
self.assertNotIn(self.user.id, team_ids, 'user should NOT be in team')
|
||||
|
||||
def test_no_auth_put_user_invite_response(self):
|
||||
invite = ProposalTeamInvite(
|
||||
|
|
|
@ -144,7 +144,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"] = '' # TODO confirm avatar is no longer a dict
|
||||
updated_user["avatar"] = ''
|
||||
updated_user["socialMedias"] = []
|
||||
|
||||
user_update_resp = self.app.put(
|
||||
|
|
|
@ -33,7 +33,6 @@ export interface VOut {
|
|||
scriptPubKey: ScriptPubKey;
|
||||
}
|
||||
|
||||
|
||||
export interface Transaction {
|
||||
txid: string;
|
||||
hex: string;
|
||||
|
@ -46,7 +45,7 @@ export interface Transaction {
|
|||
time: number;
|
||||
vin: VIn[];
|
||||
vout: VOut[];
|
||||
// TODO: fill me out, what is this?
|
||||
// unclear what vjoinsplit is
|
||||
vjoinsplit: any[];
|
||||
}
|
||||
|
||||
|
@ -74,7 +73,6 @@ export interface BlockWithTransactionIds extends Block {
|
|||
tx: string[];
|
||||
}
|
||||
|
||||
|
||||
export interface BlockWithTransactions extends Block {
|
||||
tx: Transaction[];
|
||||
}
|
||||
|
@ -107,33 +105,44 @@ export interface ValidationResponse {
|
|||
isvalid: boolean;
|
||||
}
|
||||
|
||||
|
||||
// TODO: Type all methods with signatures from
|
||||
// https://github.com/zcash/zcash/blob/master/doc/payment-api.md
|
||||
interface ZCashNode {
|
||||
getblockchaininfo: () => Promise<BlockChainInfo>;
|
||||
getblockcount: () => Promise<number>;
|
||||
getblock: {
|
||||
(numberOrHash: string | number, verbosity?: 1): Promise<BlockWithTransactionIds>;
|
||||
(numberOrHash: string | number, verbosity: 2): Promise<BlockWithTransactions>;
|
||||
(numberOrHash: string | number, verbosity?: 1): Promise<
|
||||
BlockWithTransactionIds
|
||||
>;
|
||||
(numberOrHash: string | number, verbosity: 2): Promise<
|
||||
BlockWithTransactions
|
||||
>;
|
||||
(numberOrHash: string | number, verbosity: 0): Promise<string>;
|
||||
}
|
||||
};
|
||||
gettransaction: (txid: string) => Promise<Transaction>;
|
||||
validateaddress: (address: string) => Promise<ValidationResponse>;
|
||||
z_getbalance: (address: string, minConf?: number) => Promise<number>;
|
||||
z_getnewaddress: (type?: 'sprout' | 'sapling') => Promise<string>;
|
||||
z_getnewaddress: (type?: "sprout" | "sapling") => Promise<string>;
|
||||
z_listaddresses: () => Promise<string[]>;
|
||||
z_listreceivedbyaddress: (address: string, minConf?: number) => Promise<Receipt[]>;
|
||||
z_importviewingkey: (key: string, rescan?: 'yes' | 'no' | 'whenkeyisnew', startHeight?: number) => Promise<void>;
|
||||
z_listreceivedbyaddress: (
|
||||
address: string,
|
||||
minConf?: number
|
||||
) => Promise<Receipt[]>;
|
||||
z_importviewingkey: (
|
||||
key: string,
|
||||
rescan?: "yes" | "no" | "whenkeyisnew",
|
||||
startHeight?: number
|
||||
) => Promise<void>;
|
||||
z_exportviewingkey: (zaddr: string) => Promise<string>;
|
||||
z_validatepaymentdisclosure: (disclosure: string) => Promise<DisclosedPayment>;
|
||||
z_validatepaymentdisclosure: (
|
||||
disclosure: string
|
||||
) => Promise<DisclosedPayment>;
|
||||
z_validateaddress: (address: string) => Promise<ValidationResponse>;
|
||||
}
|
||||
|
||||
export const rpcOptions = {
|
||||
url: env.ZCASH_NODE_URL,
|
||||
username: env.ZCASH_NODE_USERNAME,
|
||||
password: env.ZCASH_NODE_PASSWORD,
|
||||
password: env.ZCASH_NODE_PASSWORD
|
||||
};
|
||||
|
||||
const node: ZCashNode = stdrpc(rpcOptions);
|
||||
|
@ -152,28 +161,31 @@ export async function initNode() {
|
|||
}
|
||||
if (info.chain.includes("test")) {
|
||||
network = bitcore.Networks.testnet;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
network = bitcore.Networks.mainnet;
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
} catch (err) {
|
||||
captureException(err);
|
||||
log.error(err.response ? err.response.data : err);
|
||||
log.error('Failed to connect to zcash node with the following credentials:\r\n', rpcOptions);
|
||||
log.error(
|
||||
"Failed to connect to zcash node with the following credentials:\r\n",
|
||||
rpcOptions
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if sprout address is readable
|
||||
try {
|
||||
if (!env.SPROUT_ADDRESS) {
|
||||
console.error('Missing SPROUT_ADDRESS environment variable, exiting');
|
||||
console.error("Missing SPROUT_ADDRESS environment variable, exiting");
|
||||
process.exit(1);
|
||||
}
|
||||
await node.z_getbalance(env.SPROUT_ADDRESS as string);
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
if (!env.SPROUT_VIEWKEY) {
|
||||
log.error('Unable to view SPROUT_ADDRESS and missing SPROUT_VIEWKEY environment variable, exiting');
|
||||
log.error(
|
||||
"Unable to view SPROUT_ADDRESS and missing SPROUT_VIEWKEY environment variable, exiting"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
await node.z_importviewingkey(env.SPROUT_VIEWKEY as string);
|
||||
|
@ -183,7 +195,7 @@ export async function initNode() {
|
|||
|
||||
export function getNetwork() {
|
||||
if (!network) {
|
||||
throw new Error('Called getNetwork before initNode');
|
||||
throw new Error("Called getNetwork before initNode");
|
||||
}
|
||||
return network;
|
||||
}
|
||||
|
@ -194,11 +206,15 @@ export async function getBootstrapBlockHeight(txid: string | undefined) {
|
|||
try {
|
||||
const tx = await node.gettransaction(txid);
|
||||
const block = await node.getblock(tx.blockhash);
|
||||
const height = block.height - parseInt(env.MINIMUM_BLOCK_CONFIRMATIONS, 10);
|
||||
const height =
|
||||
block.height - parseInt(env.MINIMUM_BLOCK_CONFIRMATIONS, 10);
|
||||
return height.toString();
|
||||
} catch(err) {
|
||||
console.warn(`Attempted to get block height for tx ${txid} but failed with the following error:\n`, err);
|
||||
console.warn('Falling back to hard-coded starter blocks');
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`Attempted to get block height for tx ${txid} but failed with the following error:\n`,
|
||||
err
|
||||
);
|
||||
console.warn("Falling back to hard-coded starter blocks");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,11 +223,10 @@ export async function getBootstrapBlockHeight(txid: string | undefined) {
|
|||
const net = getNetwork();
|
||||
if (net === bitcore.Networks.mainnet) {
|
||||
return env.MAINNET_START_BLOCK;
|
||||
}
|
||||
else if (net === bitcore.Networks.testnet && !net.regtestEnabled) {
|
||||
} else if (net === bitcore.Networks.testnet && !net.regtestEnabled) {
|
||||
return env.TESTNET_START_BLOCK;
|
||||
}
|
||||
|
||||
// Regtest or otherwise unknown networks should start at the bottom
|
||||
return '0';
|
||||
return "0";
|
||||
}
|
||||
|
|
|
@ -21,8 +21,7 @@ export function authenticate(secret: string) {
|
|||
return hash === sha256(secret);
|
||||
}
|
||||
|
||||
// TODO: Not fully confident in compatibility with most bip32 wallets,
|
||||
// do more work to ensure this is reliable.
|
||||
// NOTE: this is just one way to derive t-addrs
|
||||
export function deriveTransparentAddress(index: number, network: any) {
|
||||
const root = new HDPublicKey(env.BIP32_XPUB);
|
||||
const child = root.derive(`m/0/${index}`);
|
||||
|
@ -39,14 +38,16 @@ export function removeItem<T>(arr: T[], remove: T) {
|
|||
}
|
||||
|
||||
export function encodeHexMemo(memo: string) {
|
||||
return new Buffer(memo, 'utf8').toString('hex');
|
||||
return new Buffer(memo, "utf8").toString("hex");
|
||||
}
|
||||
|
||||
export function decodeHexMemo(memoHex: string) {
|
||||
return new Buffer(memoHex, 'hex')
|
||||
.toString()
|
||||
// Remove null bytes from zero padding
|
||||
.replace(/\0.*$/g, '');
|
||||
return (
|
||||
new Buffer(memoHex, "hex")
|
||||
.toString()
|
||||
// Remove null bytes from zero padding
|
||||
.replace(/\0.*$/g, "")
|
||||
);
|
||||
}
|
||||
|
||||
export function makeContributionMemo(contributionId: number) {
|
||||
|
@ -54,14 +55,15 @@ export function makeContributionMemo(contributionId: number) {
|
|||
}
|
||||
|
||||
export function getContributionIdFromMemo(memoHex: string) {
|
||||
const matches = decodeHexMemo(memoHex).match(/Contribution ([0-9]+) on Grant\.io/);
|
||||
const matches = decodeHexMemo(memoHex).match(
|
||||
/Contribution ([0-9]+) on Grant\.io/
|
||||
);
|
||||
if (matches && matches[1]) {
|
||||
return parseInt(matches[1], 10);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Make this more robust
|
||||
export function toBaseUnit(unit: number) {
|
||||
return Math.floor(100000000 * unit);
|
||||
}
|
||||
|
|
|
@ -21,4 +21,4 @@ Tests can be found in `cypress/integration`. Cypress will hot-reload open tests
|
|||
|
||||
### CI
|
||||
|
||||
TODO
|
||||
Coming soon.
|
||||
|
|
|
@ -79,7 +79,6 @@ describe("create.fund.ms2.no-vote.re-vote", () => {
|
|||
"Request milestone payout",
|
||||
{ timeout: 20000 }
|
||||
).click();
|
||||
// TODO: fix this bug (the following fails)
|
||||
cy.contains(".MilestoneAction-progress-text", "voted against payout", {
|
||||
timeout: 20000
|
||||
});
|
||||
|
|
|
@ -25,7 +25,6 @@ class BasicHead extends React.Component<Props> {
|
|||
name="keywords"
|
||||
content="Zcash, Zcash Foundation, Zcash Foundation Grants, Zcash Grants, Zcash Grant, ZF Grants, ZFGrants"
|
||||
/>
|
||||
<meta name={`${title} page`} content={`${title} page stuff`} />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://use.fontawesome.com/releases/v5.2.0/css/all.css"
|
||||
|
@ -41,7 +40,6 @@ class BasicHead extends React.Component<Props> {
|
|||
<meta property="og:url" content={defaultOgpUrl} />
|
||||
<meta property="og:image" content={defaultOgpImage} />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
{/* TODO: i18n */}
|
||||
{/* <meta property="og:locale:alternate" content="en_US" /> */}
|
||||
{/* <meta property="og:locale:alternate" content="de_DE" /> */}
|
||||
|
||||
|
|
|
@ -41,7 +41,6 @@ class Comment extends React.Component<Props> {
|
|||
};
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
// TODO: Come up with better check on if our comment post was a success
|
||||
const { isPostCommentPending, postCommentError } = this.props;
|
||||
if (!isPostCommentPending && !postCommentError && prevProps.isPostCommentPending) {
|
||||
this.setState({ reply: '', isReplying: false });
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
// import { Helmet } from 'react-helmet';
|
||||
// import { urlToPublic } from 'utils/helpers';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { urlToPublic } from 'utils/helpers';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
|
@ -12,25 +12,22 @@ interface Props {
|
|||
|
||||
export default class HeaderDetails extends React.Component<Props> {
|
||||
render() {
|
||||
// TODO: Uncomment once helmet is fixed
|
||||
// https://github.com/nfl/react-helmet/issues/373
|
||||
return null;
|
||||
// const { title, image, url, type, description } = this.props;
|
||||
// return (
|
||||
// <Helmet>
|
||||
// <title>{`ZF Grants - ${title}`}</title>
|
||||
// {/* open graph protocol */}
|
||||
// {type && <meta property="og:type" content="website" />}
|
||||
// <meta property="og:title" content={title} />
|
||||
// {description && <meta property="og:description" content={description} />}
|
||||
// {url && <meta property="og:url" content={urlToPublic(url)} />}
|
||||
// {image && <meta property="og:image" content={urlToPublic(image)} />}
|
||||
// {/* twitter card */}
|
||||
// <meta property="twitter:title" content={title} />
|
||||
// {description && <meta property="twitter:description" content={description} />}
|
||||
// {url && <meta property="twitter:url" content={urlToPublic(url)} />}
|
||||
// {image && <meta property="twitter:image" content={urlToPublic(image)} />}
|
||||
// </Helmet>
|
||||
// );
|
||||
const { title, image, url, type, description } = this.props;
|
||||
return (
|
||||
<Helmet>
|
||||
<title>{`ZF Grants - ${title}`}</title>
|
||||
{/* open graph protocol */}
|
||||
{type && <meta property="og:type" content="website" />}
|
||||
<meta property="og:title" content={title} />
|
||||
{description && <meta property="og:description" content={description} />}
|
||||
{url && <meta property="og:url" content={urlToPublic(url)} />}
|
||||
{image && <meta property="og:image" content={urlToPublic(image)} />}
|
||||
{/* twitter card */}
|
||||
<meta property="twitter:title" content={title} />
|
||||
{description && <meta property="twitter:description" content={description} />}
|
||||
{url && <meta property="twitter:url" content={urlToPublic(url)} />}
|
||||
{image && <meta property="twitter:image" content={urlToPublic(image)} />}
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,7 +106,6 @@ class Profile extends React.Component<Props, State> {
|
|||
|
||||
return (
|
||||
<div className="Profile">
|
||||
{/* TODO: customize details for funders/creators */}
|
||||
<HeaderDetails
|
||||
title={`${user.displayName} is funding projects on ZF Grants`}
|
||||
description={`Join ${user.displayName} in funding the future!`}
|
||||
|
|
|
@ -240,7 +240,6 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
|
|||
return;
|
||||
}
|
||||
|
||||
// TODO: Get values from proposal
|
||||
const { target, funded } = this.props.proposal;
|
||||
const remainingTarget = target.sub(funded);
|
||||
const amount = parseFloat(value);
|
||||
|
|
|
@ -55,7 +55,6 @@ class ProposalComments extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
// TODO: Come up with better check on if our comment post was a success
|
||||
const { isPostCommentPending, postCommentError } = this.props;
|
||||
if (!isPostCommentPending && !postCommentError && prevProps.isPostCommentPending) {
|
||||
this.setState({ comment: '' });
|
||||
|
|
|
@ -386,8 +386,7 @@ export default (state = INITIAL_STATE, action: any) => {
|
|||
case types.PROPOSAL_UPDATES_REJECTED:
|
||||
return {
|
||||
...state,
|
||||
// TODO: Get action to send real error
|
||||
updatesError: 'Failed to fetch updates',
|
||||
updatesError: (payload && payload.message) || payload.toString(),
|
||||
isFetchingUpdates: false,
|
||||
};
|
||||
|
||||
|
@ -402,8 +401,7 @@ export default (state = INITIAL_STATE, action: any) => {
|
|||
case types.PROPOSAL_CONTRIBUTIONS_REJECTED:
|
||||
return {
|
||||
...state,
|
||||
// TODO: Get action to send real error
|
||||
fetchContributionsError: 'Failed to fetch updates',
|
||||
fetchContributionsError: (payload && payload.message) || payload.toString(),
|
||||
isFetchingContributions: false,
|
||||
};
|
||||
|
||||
|
|
|
@ -47,8 +47,7 @@ export default (state: RFPState = INITIAL_STATE, action: any): RFPState => {
|
|||
case types.FETCH_RFP_REJECTED:
|
||||
return {
|
||||
...state,
|
||||
// TODO: Get action to send real error
|
||||
fetchRfpsError: 'Failed to fetch rfps',
|
||||
fetchRfpsError: (payload && payload.message) || payload.toString(),
|
||||
isFetchingRfps: false,
|
||||
};
|
||||
case types.FETCH_RFPS_FULFILLED:
|
||||
|
|
|
@ -114,7 +114,7 @@ export function formatRFPFromGet(rfp: RFP): RFP {
|
|||
return rfp;
|
||||
}
|
||||
|
||||
// TODO: i18n on case-by-case basis
|
||||
// NOTE: i18n on case-by-case basis
|
||||
export function generateSlugUrl(id: number, title: string) {
|
||||
const slug = title
|
||||
.toLowerCase()
|
||||
|
|
|
@ -27,7 +27,6 @@ const converters: { [key in MARKDOWN_TYPE]: Showdown.Converter } = {
|
|||
...sharedOptions,
|
||||
noHeaderId: true,
|
||||
headerLevelStart: 4,
|
||||
// TODO: Find a way to disable images
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
"@types/react": "16.4.18",
|
||||
"@types/react-cropper": "^0.10.3",
|
||||
"@types/react-dom": "16.0.9",
|
||||
"@types/react-helmet": "^5.0.7",
|
||||
"@types/react-helmet": "^5.0.8",
|
||||
"@types/react-redux": "^6.0.2",
|
||||
"@types/react-router": "4.4.3",
|
||||
"@types/react-router-dom": "^4.3.1",
|
||||
|
@ -126,7 +126,7 @@
|
|||
"react-cropper": "^1.0.1",
|
||||
"react-dev-utils": "^5.0.2",
|
||||
"react-dom": "16.5.2",
|
||||
"react-helmet": "^5.2.0",
|
||||
"react-helmet": "6.0.0-beta",
|
||||
"react-hot-loader": "^4.3.8",
|
||||
"react-i18next": "^8.3.5",
|
||||
"react-mde": "7.0.4",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { ChunkExtractor } from '@loadable/server';
|
||||
|
||||
export interface Props {
|
||||
|
@ -34,13 +34,6 @@ const HTML: React.SFC<Props> = ({
|
|||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="msapplication-TileColor" content="#fff" />
|
||||
<meta name="theme-color" content="#fff" />
|
||||
{/* TODO: import from @fortawesome */}
|
||||
{/* <link
|
||||
rel="stylesheet"
|
||||
href="https://use.fontawesome.com/releases/v5.2.0/css/all.css"
|
||||
integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ"
|
||||
crossOrigin="anonymous"
|
||||
/> */}
|
||||
{/* Custom link & meta tags from webpack */}
|
||||
{extractor.getLinkElements()}
|
||||
{linkTags.map((l, idx) => (
|
||||
|
|
|
@ -13,7 +13,7 @@ export interface Contribution {
|
|||
export interface ContributionWithAddresses extends Contribution {
|
||||
addresses: {
|
||||
transparent: string;
|
||||
// TODO: Add sapling and memo in when ready
|
||||
// NOTE: Add sapling and memo in when ready
|
||||
// sprout: string;
|
||||
// memo: string;
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ import { SocialMedia } from 'types';
|
|||
|
||||
export interface User {
|
||||
userid: number;
|
||||
emailAddress?: string; // TODO: Split into full user type
|
||||
emailAddress?: string;
|
||||
emailVerified?: boolean;
|
||||
displayName: string;
|
||||
title: string;
|
||||
|
|
|
@ -1940,9 +1940,10 @@
|
|||
"@types/node" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-helmet@^5.0.7":
|
||||
version "5.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-5.0.7.tgz#1cae65b2c37fe54cf56f40cd388836d4619dbc51"
|
||||
"@types/react-helmet@^5.0.8":
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-5.0.8.tgz#f080eea6652e44f60b4574463d238f268d81d9af"
|
||||
integrity sha512-ZTr12eDAYI0yUiMx1K82EHqRYa8J1BOOLus+0gL+AkksUiIPwLE0wLiXa9FNqD8r9GXAi+yRPZImkRh1JNlTkQ==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
|
@ -9601,6 +9602,11 @@ react-error-overlay@^4.0.1:
|
|||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.1.tgz#417addb0814a90f3a7082eacba7cee588d00da89"
|
||||
|
||||
react-fast-compare@^2.0.2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||
|
||||
react-fittext@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-fittext/-/react-fittext-1.0.0.tgz#836a1c04f9322f6c94cb69e45c66006fc42d37a5"
|
||||
|
@ -9617,13 +9623,14 @@ react-fuzzy@^0.5.2:
|
|||
fuse.js "^3.0.1"
|
||||
prop-types "^15.5.9"
|
||||
|
||||
react-helmet@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-5.2.0.tgz#a81811df21313a6d55c5f058c4aeba5d6f3d97a7"
|
||||
react-helmet@6.0.0-beta:
|
||||
version "6.0.0-beta"
|
||||
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.0.0-beta.tgz#1f2ac04521951486e4fce3296d0c88aae8cabd5c"
|
||||
integrity sha512-GnNWsokebTe7fe8MH2I/a2dl4THYWhthLBoMaQSRYqW5XbPo881WAJGi+lqRBjyOFryW6zpQluEkBy70zh+h9w==
|
||||
dependencies:
|
||||
deep-equal "^1.0.1"
|
||||
object-assign "^4.1.1"
|
||||
prop-types "^15.5.4"
|
||||
react-fast-compare "^2.0.2"
|
||||
react-side-effect "^1.1.0"
|
||||
|
||||
react-hot-loader@^4.3.8:
|
||||
|
|
Loading…
Reference in New Issue