Merge branch 'develop' into task-scheduler
This commit is contained in:
commit
357517278d
|
@ -27,3 +27,7 @@ BLOCKCHAIN_API_SECRET="ef0b48e41f78d3ae85b1379b386f1bca"
|
||||||
|
|
||||||
# run `flask gen-admin-auth` to create new password for admin
|
# run `flask gen-admin-auth` to create new password for admin
|
||||||
ADMIN_PASS_HASH=18f97883b93a975deb9e29257a341a447302040da59cdc2d10ff65a5e57cc197
|
ADMIN_PASS_HASH=18f97883b93a975deb9e29257a341a447302040da59cdc2d10ff65a5e57cc197
|
||||||
|
|
||||||
|
# Blockchain explorer to link to. Top for mainnet, bottom for testnet.
|
||||||
|
# EXPLORER_URL="https://explorer.zcha.in/"
|
||||||
|
EXPLORER_URL="https://testnet.zcha.in/"
|
||||||
|
|
|
@ -198,18 +198,13 @@ class Proposal(db.Model):
|
||||||
if category and category not in CATEGORIES:
|
if category and category not in CATEGORIES:
|
||||||
raise ValidationException("Category {} not in {}".format(category, CATEGORIES))
|
raise ValidationException("Category {} not in {}".format(category, CATEGORIES))
|
||||||
|
|
||||||
def validate_publishable(self, current_user=None):
|
def validate_publishable(self):
|
||||||
# Require certain fields
|
# Require certain fields
|
||||||
required_fields = ['title', 'content', 'brief', 'category', 'target', 'payout_address']
|
required_fields = ['title', 'content', 'brief', 'category', 'target', 'payout_address']
|
||||||
for field in required_fields:
|
for field in required_fields:
|
||||||
if not hasattr(self, field):
|
if not hasattr(self, field):
|
||||||
raise ValidationException("Proposal must have a {}".format(field))
|
raise ValidationException("Proposal must have a {}".format(field))
|
||||||
|
|
||||||
if current_user:
|
|
||||||
if not current_user.email_verification.has_verified:
|
|
||||||
message = "Please confirm your email before attempting to publish a proposal."
|
|
||||||
raise ValidationException(message)
|
|
||||||
|
|
||||||
# Then run through regular validation
|
# Then run through regular validation
|
||||||
Proposal.validate(vars(self))
|
Proposal.validate(vars(self))
|
||||||
|
|
||||||
|
@ -256,8 +251,8 @@ class Proposal(db.Model):
|
||||||
self.deadline_duration = deadline_duration
|
self.deadline_duration = deadline_duration
|
||||||
Proposal.validate(vars(self))
|
Proposal.validate(vars(self))
|
||||||
|
|
||||||
def submit_for_approval(self, current_user):
|
def submit_for_approval(self):
|
||||||
self.validate_publishable(current_user)
|
self.validate_publishable()
|
||||||
allowed_statuses = [DRAFT, REJECTED]
|
allowed_statuses = [DRAFT, REJECTED]
|
||||||
# specific validation
|
# specific validation
|
||||||
if self.status not in allowed_statuses:
|
if self.status not in allowed_statuses:
|
||||||
|
|
|
@ -4,6 +4,7 @@ from flask_yoloapi import endpoint, parameter
|
||||||
from grant.comment.models import Comment, comment_schema, comments_schema
|
from grant.comment.models import Comment, comment_schema, comments_schema
|
||||||
from grant.email.send import send_email
|
from grant.email.send import send_email
|
||||||
from grant.milestone.models import Milestone
|
from grant.milestone.models import Milestone
|
||||||
|
from grant.settings import EXPLORER_URL
|
||||||
from grant.user.models import User
|
from grant.user.models import User
|
||||||
from grant.utils.auth import requires_auth, requires_team_member_auth, get_authed_user, internal_webhook
|
from grant.utils.auth import requires_auth, requires_team_member_auth, get_authed_user, internal_webhook
|
||||||
from grant.utils.exceptions import ValidationException
|
from grant.utils.exceptions import ValidationException
|
||||||
|
@ -232,7 +233,7 @@ def delete_proposal(proposal_id):
|
||||||
@endpoint.api()
|
@endpoint.api()
|
||||||
def submit_for_approval_proposal(proposal_id):
|
def submit_for_approval_proposal(proposal_id):
|
||||||
try:
|
try:
|
||||||
g.current_proposal.submit_for_approval(current_user=g.current_user)
|
g.current_proposal.submit_for_approval()
|
||||||
except ValidationException as e:
|
except ValidationException as e:
|
||||||
return {"message": "{}".format(str(e))}, 400
|
return {"message": "{}".format(str(e))}, 400
|
||||||
db.session.add(g.current_proposal)
|
db.session.add(g.current_proposal)
|
||||||
|
@ -455,7 +456,7 @@ def post_contribution_confirmation(contribution_id, to, amount, txid):
|
||||||
send_email(contribution.user.email_address, 'contribution_confirmed', {
|
send_email(contribution.user.email_address, 'contribution_confirmed', {
|
||||||
'contribution': contribution,
|
'contribution': contribution,
|
||||||
'proposal': contribution.proposal,
|
'proposal': contribution.proposal,
|
||||||
'tx_explorer_url': f'https://explorer.zcha.in/transactions/{txid}',
|
'tx_explorer_url': f'{EXPLORER_URL}transactions/{txid}',
|
||||||
})
|
})
|
||||||
|
|
||||||
# Send to the full proposal gang
|
# Send to the full proposal gang
|
||||||
|
|
|
@ -52,6 +52,8 @@ BLOCKCHAIN_API_SECRET = env.str("BLOCKCHAIN_API_SECRET")
|
||||||
|
|
||||||
ADMIN_PASS_HASH = env.str("ADMIN_PASS_HASH")
|
ADMIN_PASS_HASH = env.str("ADMIN_PASS_HASH")
|
||||||
|
|
||||||
|
EXPLORER_URL = env.str("EXPLORER_URL", default="https://explorer.zcha.in/")
|
||||||
|
|
||||||
UI = {
|
UI = {
|
||||||
'NAME': 'ZF Grants',
|
'NAME': 'ZF Grants',
|
||||||
'PRIMARY': '#CF8A00',
|
'PRIMARY': '#CF8A00',
|
||||||
|
|
|
@ -234,16 +234,21 @@ class SelfUserSchema(ma.Schema):
|
||||||
"social_medias",
|
"social_medias",
|
||||||
"avatar",
|
"avatar",
|
||||||
"display_name",
|
"display_name",
|
||||||
"userid"
|
"userid",
|
||||||
|
"email_verified"
|
||||||
)
|
)
|
||||||
|
|
||||||
social_medias = ma.Nested("SocialMediaSchema", many=True)
|
social_medias = ma.Nested("SocialMediaSchema", many=True)
|
||||||
avatar = ma.Nested("AvatarSchema")
|
avatar = ma.Nested("AvatarSchema")
|
||||||
userid = ma.Method("get_userid")
|
userid = ma.Method("get_userid")
|
||||||
|
email_verified = ma.Method("get_email_verified")
|
||||||
|
|
||||||
def get_userid(self, obj):
|
def get_userid(self, obj):
|
||||||
return obj.id
|
return obj.id
|
||||||
|
|
||||||
|
def get_email_verified(self, obj):
|
||||||
|
return obj.email_verification.has_verified
|
||||||
|
|
||||||
|
|
||||||
self_user_schema = SelfUserSchema()
|
self_user_schema = SelfUserSchema()
|
||||||
self_users_schema = SelfUserSchema(many=True)
|
self_users_schema = SelfUserSchema(many=True)
|
||||||
|
@ -258,19 +263,25 @@ class UserSchema(ma.Schema):
|
||||||
"social_medias",
|
"social_medias",
|
||||||
"avatar",
|
"avatar",
|
||||||
"display_name",
|
"display_name",
|
||||||
"userid"
|
"userid",
|
||||||
|
"email_verified"
|
||||||
)
|
)
|
||||||
|
|
||||||
social_medias = ma.Nested("SocialMediaSchema", many=True)
|
social_medias = ma.Nested("SocialMediaSchema", many=True)
|
||||||
avatar = ma.Nested("AvatarSchema")
|
avatar = ma.Nested("AvatarSchema")
|
||||||
userid = ma.Method("get_userid")
|
userid = ma.Method("get_userid")
|
||||||
|
email_verified = ma.Method("get_email_verified")
|
||||||
|
|
||||||
def get_userid(self, obj):
|
def get_userid(self, obj):
|
||||||
return obj.id
|
return obj.id
|
||||||
|
|
||||||
|
def get_email_verified(self, obj):
|
||||||
|
return obj.email_verification.has_verified
|
||||||
|
|
||||||
|
|
||||||
|
user_schema = UserSchema()
|
||||||
|
users_schema = UserSchema(many=True)
|
||||||
|
|
||||||
user_schema = SelfUserSchema()
|
|
||||||
users_schema = SelfUserSchema(many=True)
|
|
||||||
|
|
||||||
class SocialMediaSchema(ma.Schema):
|
class SocialMediaSchema(ma.Schema):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -57,9 +57,12 @@ def requires_team_member_auth(f):
|
||||||
if not proposal:
|
if not proposal:
|
||||||
return jsonify(message="No proposal exists with id {}".format(proposal_id)), 404
|
return jsonify(message="No proposal exists with id {}".format(proposal_id)), 404
|
||||||
|
|
||||||
if not g.current_user in proposal.team:
|
if g.current_user not in proposal.team:
|
||||||
return jsonify(message="You are not authorized to modify this proposal"), 403
|
return jsonify(message="You are not authorized to modify this proposal"), 403
|
||||||
|
|
||||||
|
if not g.current_user.email_verification.has_verified:
|
||||||
|
return jsonify(message="Please confirm your email."), 403
|
||||||
|
|
||||||
g.current_proposal = proposal
|
g.current_proposal = proposal
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,7 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
||||||
def test_approve_proposal(self):
|
def test_approve_proposal(self):
|
||||||
self.login_admin()
|
self.login_admin()
|
||||||
# submit for approval (performed by end-user)
|
# submit for approval (performed by end-user)
|
||||||
self.proposal.submit_for_approval(self.user)
|
self.proposal.submit_for_approval()
|
||||||
# approve
|
# approve
|
||||||
resp = self.app.put(
|
resp = self.app.put(
|
||||||
"/api/v1/admin/proposals/{}/approve".format(self.proposal.id),
|
"/api/v1/admin/proposals/{}/approve".format(self.proposal.id),
|
||||||
|
@ -90,7 +90,7 @@ class TestAdminAPI(BaseProposalCreatorConfig):
|
||||||
def test_reject_proposal(self):
|
def test_reject_proposal(self):
|
||||||
self.login_admin()
|
self.login_admin()
|
||||||
# submit for approval (performed by end-user)
|
# submit for approval (performed by end-user)
|
||||||
self.proposal.submit_for_approval(self.user)
|
self.proposal.submit_for_approval()
|
||||||
# reject
|
# reject
|
||||||
resp = self.app.put(
|
resp = self.app.put(
|
||||||
"/api/v1/admin/proposals/{}/approve".format(self.proposal.id),
|
"/api/v1/admin/proposals/{}/approve".format(self.proposal.id),
|
||||||
|
|
|
@ -89,7 +89,7 @@ class TestProposalAPI(BaseProposalCreatorConfig):
|
||||||
def test_publish_proposal_approved(self):
|
def test_publish_proposal_approved(self):
|
||||||
self.login_default_user()
|
self.login_default_user()
|
||||||
# submit for approval, then approve
|
# submit for approval, then approve
|
||||||
self.proposal.submit_for_approval(self.user)
|
self.proposal.submit_for_approval()
|
||||||
self.proposal.approve_pending(True) # admin action
|
self.proposal.approve_pending(True) # admin action
|
||||||
resp = self.app.put("/api/v1/proposals/{}/publish".format(self.proposal.id))
|
resp = self.app.put("/api/v1/proposals/{}/publish".format(self.proposal.id))
|
||||||
self.assert200(resp)
|
self.assert200(resp)
|
||||||
|
@ -114,7 +114,7 @@ class TestProposalAPI(BaseProposalCreatorConfig):
|
||||||
self.mark_user_not_verified()
|
self.mark_user_not_verified()
|
||||||
self.proposal.status = "DRAFT"
|
self.proposal.status = "DRAFT"
|
||||||
resp = self.app.put("/api/v1/proposals/{}/publish".format(self.proposal.id))
|
resp = self.app.put("/api/v1/proposals/{}/publish".format(self.proposal.id))
|
||||||
self.assert400(resp)
|
self.assert403(resp)
|
||||||
|
|
||||||
# /
|
# /
|
||||||
def test_get_proposals(self):
|
def test_get_proposals(self):
|
||||||
|
|
|
@ -63,7 +63,6 @@ class TestUserAPI(BaseUserConfig):
|
||||||
}),
|
}),
|
||||||
content_type="application/json"
|
content_type="application/json"
|
||||||
)
|
)
|
||||||
print(user_auth_resp.headers)
|
|
||||||
self.assertEqual(user_auth_resp.json['emailAddress'], self.user.email_address)
|
self.assertEqual(user_auth_resp.json['emailAddress'], self.user.email_address)
|
||||||
self.assertEqual(user_auth_resp.json['displayName'], self.user.display_name)
|
self.assertEqual(user_auth_resp.json['displayName'], self.user.display_name)
|
||||||
|
|
||||||
|
@ -76,20 +75,24 @@ class TestUserAPI(BaseUserConfig):
|
||||||
}),
|
}),
|
||||||
content_type="application/json"
|
content_type="application/json"
|
||||||
)
|
)
|
||||||
print(login_resp.headers)
|
|
||||||
# should have session cookie now
|
# should have session cookie now
|
||||||
me_resp = self.app.get(
|
me_resp = self.app.get(
|
||||||
"/api/v1/users/me"
|
"/api/v1/users/me"
|
||||||
)
|
)
|
||||||
print(me_resp.headers)
|
|
||||||
self.assert200(me_resp)
|
self.assert200(me_resp)
|
||||||
|
|
||||||
|
def test_me_get_includes_email_address(self):
|
||||||
|
self.login_default_user()
|
||||||
|
me_resp = self.app.get(
|
||||||
|
"/api/v1/users/me"
|
||||||
|
)
|
||||||
|
self.assert200(me_resp)
|
||||||
|
self.assertIsNotNone(me_resp.json['emailAddress'])
|
||||||
|
|
||||||
def test_user_auth_required_fail(self):
|
def test_user_auth_required_fail(self):
|
||||||
me_resp = self.app.get(
|
me_resp = self.app.get(
|
||||||
"/api/v1/users/me",
|
"/api/v1/users/me",
|
||||||
)
|
)
|
||||||
print(me_resp.json)
|
|
||||||
print(me_resp.headers)
|
|
||||||
self.assert401(me_resp)
|
self.assert401(me_resp)
|
||||||
|
|
||||||
def test_user_auth_bad_password(self):
|
def test_user_auth_bad_password(self):
|
||||||
|
|
|
@ -11,3 +11,7 @@ BACKEND_URL=http://localhost:5000
|
||||||
# sentry
|
# sentry
|
||||||
# SENTRY_DSN=https://PUBLICKEY@sentry.io/PROJECTID
|
# SENTRY_DSN=https://PUBLICKEY@sentry.io/PROJECTID
|
||||||
# SENTRY_RELEASE="optional, provides sentry logging with release info"
|
# SENTRY_RELEASE="optional, provides sentry logging with release info"
|
||||||
|
|
||||||
|
# Blockchain explorer to link to. Top for mainnet, bottom for testnet.
|
||||||
|
# EXPLORER_URL="https://explorer.zcha.in/"
|
||||||
|
EXPLORER_URL="https://testnet.zcha.in/"
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Spin, List, Button, Divider, Popconfirm, message } from 'antd';
|
import { Button, Divider, List, message, Popconfirm, Spin } from 'antd';
|
||||||
import Placeholder from 'components/Placeholder';
|
import Placeholder from 'components/Placeholder';
|
||||||
|
import { getIsVerified } from 'modules/auth/selectors';
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
import { ProposalDraft, STATUS } from 'types';
|
import { ProposalDraft, STATUS } from 'types';
|
||||||
import { fetchDrafts, createDraft, deleteDraft } from 'modules/create/actions';
|
import { createDraft, deleteDraft, fetchDrafts } from 'modules/create/actions';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import './style.less';
|
import './style.less';
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ interface StateProps {
|
||||||
createDraftError: AppState['create']['createDraftError'];
|
createDraftError: AppState['create']['createDraftError'];
|
||||||
isDeletingDraft: AppState['create']['isDeletingDraft'];
|
isDeletingDraft: AppState['create']['isDeletingDraft'];
|
||||||
deleteDraftError: AppState['create']['deleteDraftError'];
|
deleteDraftError: AppState['create']['deleteDraftError'];
|
||||||
|
isVerified: ReturnType<typeof getIsVerified>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
|
@ -51,8 +53,9 @@ class DraftList extends React.Component<Props, State> {
|
||||||
isDeletingDraft,
|
isDeletingDraft,
|
||||||
deleteDraftError,
|
deleteDraftError,
|
||||||
createDraftError,
|
createDraftError,
|
||||||
|
isVerified,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
if (createIfNone && drafts && !prevProps.drafts && !drafts.length) {
|
if (isVerified && createIfNone && drafts && !prevProps.drafts && !drafts.length) {
|
||||||
this.createDraft();
|
this.createDraft();
|
||||||
}
|
}
|
||||||
if (prevProps.isDeletingDraft && !isDeletingDraft) {
|
if (prevProps.isDeletingDraft && !isDeletingDraft) {
|
||||||
|
@ -67,9 +70,20 @@ class DraftList extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { drafts, isCreatingDraft } = this.props;
|
const { drafts, isCreatingDraft, isVerified } = this.props;
|
||||||
const { deletingId } = this.state;
|
const { deletingId } = this.state;
|
||||||
|
|
||||||
|
if (!isVerified) {
|
||||||
|
return (
|
||||||
|
<div className="DraftList">
|
||||||
|
<Placeholder
|
||||||
|
title="Your email is not verified"
|
||||||
|
subtitle="Please confirm your email before making a proposal."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!drafts || isCreatingDraft) {
|
if (!drafts || isCreatingDraft) {
|
||||||
return <Loader size="large" />;
|
return <Loader size="large" />;
|
||||||
}
|
}
|
||||||
|
@ -158,6 +172,7 @@ export default connect<StateProps, DispatchProps, OwnProps, AppState>(
|
||||||
createDraftError: state.create.createDraftError,
|
createDraftError: state.create.createDraftError,
|
||||||
isDeletingDraft: state.create.isDeletingDraft,
|
isDeletingDraft: state.create.isDeletingDraft,
|
||||||
deleteDraftError: state.create.deleteDraftError,
|
deleteDraftError: state.create.deleteDraftError,
|
||||||
|
isVerified: getIsVerified(state),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
fetchDrafts,
|
fetchDrafts,
|
||||||
|
|
|
@ -5,11 +5,11 @@ import { AppState } from 'store/reducers';
|
||||||
import { Proposal } from 'types';
|
import { Proposal } from 'types';
|
||||||
import { fetchProposalComments, postProposalComment } from 'modules/proposals/actions';
|
import { fetchProposalComments, postProposalComment } from 'modules/proposals/actions';
|
||||||
import {
|
import {
|
||||||
getProposalComments,
|
|
||||||
getIsFetchingComments,
|
|
||||||
getCommentsError,
|
getCommentsError,
|
||||||
|
getIsFetchingComments,
|
||||||
|
getProposalComments,
|
||||||
} from 'modules/proposals/selectors';
|
} from 'modules/proposals/selectors';
|
||||||
import { getIsSignedIn } from 'modules/auth/selectors';
|
import { getIsVerified } from 'modules/auth/selectors';
|
||||||
import Comments from 'components/Comments';
|
import Comments from 'components/Comments';
|
||||||
import Placeholder from 'components/Placeholder';
|
import Placeholder from 'components/Placeholder';
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
|
@ -26,7 +26,7 @@ interface StateProps {
|
||||||
commentsError: ReturnType<typeof getCommentsError>;
|
commentsError: ReturnType<typeof getCommentsError>;
|
||||||
isPostCommentPending: AppState['proposal']['isPostCommentPending'];
|
isPostCommentPending: AppState['proposal']['isPostCommentPending'];
|
||||||
postCommentError: AppState['proposal']['postCommentError'];
|
postCommentError: AppState['proposal']['postCommentError'];
|
||||||
isSignedIn: ReturnType<typeof getIsSignedIn>;
|
isVerified: ReturnType<typeof getIsVerified>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
|
@ -78,7 +78,7 @@ class ProposalComments extends React.Component<Props, State> {
|
||||||
isFetchingComments,
|
isFetchingComments,
|
||||||
commentsError,
|
commentsError,
|
||||||
isPostCommentPending,
|
isPostCommentPending,
|
||||||
isSignedIn,
|
isVerified,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { comment } = this.state;
|
const { comment } = this.state;
|
||||||
let content = null;
|
let content = null;
|
||||||
|
@ -106,26 +106,33 @@ class ProposalComments extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
{isSignedIn && (
|
<div className="ProposalComments-post">
|
||||||
<div className="ProposalComments-post">
|
{isVerified ? (
|
||||||
<MarkdownEditor
|
<>
|
||||||
ref={el => (this.editor = el)}
|
<MarkdownEditor
|
||||||
onChange={this.handleCommentChange}
|
ref={el => (this.editor = el)}
|
||||||
type={MARKDOWN_TYPE.REDUCED}
|
onChange={this.handleCommentChange}
|
||||||
|
type={MARKDOWN_TYPE.REDUCED}
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: '0.5rem' }} />
|
||||||
|
<Button
|
||||||
|
onClick={this.postComment}
|
||||||
|
disabled={!comment.length}
|
||||||
|
loading={isPostCommentPending}
|
||||||
|
>
|
||||||
|
Submit comment
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Placeholder
|
||||||
|
title="Your email is not verified"
|
||||||
|
subtitle="Please verify your email to post a comment."
|
||||||
/>
|
/>
|
||||||
<div style={{ marginTop: '0.5rem' }} />
|
)}
|
||||||
<Button
|
</div>
|
||||||
onClick={this.postComment}
|
|
||||||
disabled={!comment.length}
|
|
||||||
loading={isPostCommentPending}
|
|
||||||
>
|
|
||||||
Submit comment
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{content}
|
{content}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +152,7 @@ export default connect<StateProps, DispatchProps, OwnProps, AppState>(
|
||||||
commentsError: getCommentsError(state),
|
commentsError: getCommentsError(state),
|
||||||
isPostCommentPending: state.proposal.isPostCommentPending,
|
isPostCommentPending: state.proposal.isPostCommentPending,
|
||||||
postCommentError: state.proposal.postCommentError,
|
postCommentError: state.proposal.postCommentError,
|
||||||
isSignedIn: getIsSignedIn(state),
|
isVerified: getIsVerified(state),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
fetchProposalComments,
|
fetchProposalComments,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { AppState as S } from 'store/reducers';
|
import { AppState as S } from 'store/reducers';
|
||||||
|
|
||||||
|
export const getIsVerified = (s: S) => !!s.auth.user && s.auth.user.emailVerified;
|
||||||
export const getIsSignedIn = (s: S) => !!s.auth.user;
|
export const getIsSignedIn = (s: S) => !!s.auth.user;
|
||||||
export const getAuthSignature = (s: S) => s.auth.authSignature;
|
export const getAuthSignature = (s: S) => s.auth.authSignature;
|
||||||
export const getAuthSignatureAddress = (s: S) => s.auth.authSignatureAddress;
|
export const getAuthSignatureAddress = (s: S) => s.auth.authSignatureAddress;
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
import { User, UserProposal, UserComment, UserContribution, TeamInviteWithProposal } from 'types';
|
import {
|
||||||
|
User,
|
||||||
|
UserProposal,
|
||||||
|
UserComment,
|
||||||
|
UserContribution,
|
||||||
|
TeamInviteWithProposal,
|
||||||
|
} from 'types';
|
||||||
import types from './types';
|
import types from './types';
|
||||||
|
|
||||||
export interface TeamInviteWithResponse extends TeamInviteWithProposal {
|
export interface TeamInviteWithResponse extends TeamInviteWithProposal {
|
||||||
|
@ -32,6 +38,7 @@ export const INITIAL_USER: User = {
|
||||||
avatar: null,
|
avatar: null,
|
||||||
displayName: '',
|
displayName: '',
|
||||||
emailAddress: '',
|
emailAddress: '',
|
||||||
|
emailVerified: false,
|
||||||
socialMedias: [],
|
socialMedias: [],
|
||||||
title: '',
|
title: '',
|
||||||
};
|
};
|
||||||
|
@ -153,7 +160,7 @@ export default (state = INITIAL_STATE, action: any) => {
|
||||||
case types.DELETE_CONTRIBUTION:
|
case types.DELETE_CONTRIBUTION:
|
||||||
return updateUserState(state, payload.userId, {
|
return updateUserState(state, payload.userId, {
|
||||||
contributions: state.map[payload.userId].contributions.filter(
|
contributions: state.map[payload.userId].contributions.filter(
|
||||||
c => c.id !== payload.contributionId
|
c => c.id !== payload.contributionId,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
// proposal delete
|
// proposal delete
|
||||||
|
|
|
@ -87,5 +87,5 @@ export function formatZcashCLI(address: string, amount?: string | number, memo?:
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatTxExplorerUrl(txid: string) {
|
export function formatTxExplorerUrl(txid: string) {
|
||||||
return `https://explorer.zcha.in/transactions/${txid}`;
|
return `${process.env.EXPLORER_URL}transactions/${txid}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,14 +41,6 @@ envProductionRequiredHandler(
|
||||||
'http://localhost:' + (process.env.PORT || 3000),
|
'http://localhost:' + (process.env.PORT || 3000),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!process.env.SENTRY_RELEASE) {
|
|
||||||
process.env.SENTRY_RELEASE = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.env.BACKEND_URL) {
|
|
||||||
process.env.BACKEND_URL = 'http://localhost:5000';
|
|
||||||
}
|
|
||||||
|
|
||||||
const appDirectory = fs.realpathSync(process.cwd());
|
const appDirectory = fs.realpathSync(process.cwd());
|
||||||
process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
||||||
.split(path.delimiter)
|
.split(path.delimiter)
|
||||||
|
@ -58,12 +50,13 @@ process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
const raw = {
|
const raw = {
|
||||||
BACKEND_URL: process.env.BACKEND_URL,
|
BACKEND_URL: process.env.BACKEND_URL || 'http://localhost:5000',
|
||||||
|
EXPLORER_URL: process.env.EXPLORER_URL || 'https://chain.so/zcash/',
|
||||||
NODE_ENV: process.env.NODE_ENV || 'development',
|
NODE_ENV: process.env.NODE_ENV || 'development',
|
||||||
PORT: process.env.PORT || 3000,
|
PORT: process.env.PORT || 3000,
|
||||||
PUBLIC_HOST_URL: process.env.PUBLIC_HOST_URL,
|
PUBLIC_HOST_URL: process.env.PUBLIC_HOST_URL,
|
||||||
SENTRY_DSN: process.env.SENTRY_DSN || null,
|
SENTRY_DSN: process.env.SENTRY_DSN || null,
|
||||||
SENTRY_RELEASE: process.env.SENTRY_RELEASE,
|
SENTRY_RELEASE: process.env.SENTRY_RELEASE || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Stringify all values so we can feed into Webpack DefinePlugin
|
// Stringify all values so we can feed into Webpack DefinePlugin
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { SocialMedia } from 'types';
|
||||||
export interface User {
|
export interface User {
|
||||||
userid: number;
|
userid: number;
|
||||||
emailAddress?: string; // TODO: Split into full user type
|
emailAddress?: string; // TODO: Split into full user type
|
||||||
|
emailVerified?: boolean;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
title: string;
|
title: string;
|
||||||
socialMedias: SocialMedia[];
|
socialMedias: SocialMedia[];
|
||||||
|
|
Loading…
Reference in New Issue