admin notifications
This commit is contained in:
parent
426b397b3d
commit
381722de74
|
@ -60,12 +60,14 @@ export default [
|
|||
{
|
||||
id: 'proposal_failed',
|
||||
title: 'Proposal failed',
|
||||
description: 'Sent to the proposal team when the deadline is reached and it didn’t get fully funded',
|
||||
description:
|
||||
'Sent to the proposal team when the deadline is reached and it didn’t get fully funded',
|
||||
},
|
||||
{
|
||||
id: 'proposal_canceled',
|
||||
title: 'Proposal canceled',
|
||||
description: 'Sent to the proposal team when an admin cancels the proposal after funding',
|
||||
description:
|
||||
'Sent to the proposal team when an admin cancels the proposal after funding',
|
||||
},
|
||||
{
|
||||
id: 'contribution_confirmed',
|
||||
|
@ -85,7 +87,8 @@ export default [
|
|||
{
|
||||
id: 'contribution_proposal_failed',
|
||||
title: 'Contribution proposal failed',
|
||||
description: 'Sent to contributors when the deadline is reached and the proposal didn’t get fully funded',
|
||||
description:
|
||||
'Sent to contributors when the deadline is reached and the proposal didn’t get fully funded',
|
||||
},
|
||||
{
|
||||
id: 'contribution_proposal_canceled',
|
||||
|
@ -127,4 +130,19 @@ export default [
|
|||
title: 'Milestone paid',
|
||||
description: 'Sent when milestone is paid',
|
||||
},
|
||||
{
|
||||
id: 'admin_approval',
|
||||
title: 'Admin Approval',
|
||||
description: 'Sent when proposal is ready for review',
|
||||
},
|
||||
{
|
||||
id: 'admin_arbiter',
|
||||
title: 'Admin Arbiter',
|
||||
description: 'Sent when proposal is ready to have an arbiter nominated',
|
||||
},
|
||||
{
|
||||
id: 'admin_payout',
|
||||
title: 'Admin Payout',
|
||||
description: 'Sent when milestone payout has been approved',
|
||||
},
|
||||
] as Email[];
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
FLASK_APP=app.py
|
||||
FLASK_ENV=development
|
||||
SITE_URL="https://zfnd.org" # No trailing slash
|
||||
ADMIN_SITE_URL="https://grants-admin.zfnd.org" # No trailing slash
|
||||
DATABASE_URL="sqlite:////tmp/dev.db"
|
||||
REDISTOGO_URL="redis://localhost:6379"
|
||||
SECRET_KEY="not-so-secret"
|
||||
|
|
|
@ -165,5 +165,17 @@ example_email_args = {
|
|||
'amount': '33',
|
||||
'tx_explorer_url': 'http://someblockexplorer.com/tx/271857129857192579125',
|
||||
'proposal_milestones_url': 'http://zfnd.org/proposals/999-my-proposal?tab=milestones',
|
||||
}
|
||||
},
|
||||
'admin_approval': {
|
||||
'proposal': proposal,
|
||||
'proposal_url': 'https://grants-admin.zfnd.org/proposals/999',
|
||||
},
|
||||
'admin_arbiter': {
|
||||
'proposal': proposal,
|
||||
'proposal_url': 'https://grants-admin.zfnd.org/proposals/999',
|
||||
},
|
||||
'admin_payout': {
|
||||
'proposal': proposal,
|
||||
'proposal_url': 'https://grants-admin.zfnd.org/proposals/999',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -278,6 +278,33 @@ def milestone_paid(email_args):
|
|||
}
|
||||
|
||||
|
||||
def admin_approval(email_args):
|
||||
return {
|
||||
'subject': f'Review needed for {email_args["proposal"].title}',
|
||||
'title': f'Proposal Review',
|
||||
'preview': f'{email_args["proposal"].title} needs review, as an admin you can help.',
|
||||
'subscription': EmailSubscription.ADMIN_APPROVAL,
|
||||
}
|
||||
|
||||
|
||||
def admin_arbiter(email_args):
|
||||
return {
|
||||
'subject': f'Arbiter needed for {email_args["proposal"].title}',
|
||||
'title': f'Arbiter Nomination',
|
||||
'preview': f'{email_args["proposal"].title} needs an arbiter, as an admin you can help.',
|
||||
'subscription': EmailSubscription.ADMIN_ARBITER,
|
||||
}
|
||||
|
||||
|
||||
def admin_payout(email_args):
|
||||
return {
|
||||
'subject': f'Payout requested for {email_args["proposal"].title}',
|
||||
'title': f'Milestone Payout Requested',
|
||||
'preview': f'{email_args["proposal"].title} has requested a payout, as an admin you can help.',
|
||||
'subscription': EmailSubscription.ADMIN_PAYOUT,
|
||||
}
|
||||
|
||||
|
||||
get_info_lookup = {
|
||||
'signup': signup_info,
|
||||
'team_invite': team_invite_info,
|
||||
|
@ -303,7 +330,10 @@ get_info_lookup = {
|
|||
'milestone_request': milestone_request,
|
||||
'milestone_reject': milestone_reject,
|
||||
'milestone_accept': milestone_accept,
|
||||
'milestone_paid': milestone_paid
|
||||
'milestone_paid': milestone_paid,
|
||||
'admin_approval': admin_approval,
|
||||
'admin_arbiter': admin_arbiter,
|
||||
'admin_payout': admin_payout
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -53,6 +53,18 @@ class EmailSubscription(Enum):
|
|||
'bit': 11,
|
||||
'key': 'arbiter'
|
||||
}
|
||||
ADMIN_APPROVAL = {
|
||||
'bit': 12,
|
||||
'key': 'admin_approval'
|
||||
}
|
||||
ADMIN_ARBITER = {
|
||||
'bit': 13,
|
||||
'key': 'admin_arbiter'
|
||||
}
|
||||
ADMIN_PAYOUT = {
|
||||
'bit': 14,
|
||||
'key': 'admin_payout'
|
||||
}
|
||||
|
||||
|
||||
def is_email_sub_key(k: str):
|
||||
|
|
|
@ -97,6 +97,7 @@ class Milestone(db.Model):
|
|||
|
||||
def accept_immediate(self):
|
||||
if self.immediate_payout and self.index == 0:
|
||||
self.proposal.send_admin_email('admin_payout')
|
||||
self.date_requested = datetime.datetime.now()
|
||||
self.stage = MilestoneStage.ACCEPTED
|
||||
self.date_accepted = datetime.datetime.now()
|
||||
|
@ -106,6 +107,7 @@ class Milestone(db.Model):
|
|||
def accept_request(self, arbiter_id: int):
|
||||
if self.stage != MilestoneStage.REQUESTED:
|
||||
raise MilestoneException(f'Cannot accept payout request for milestone at {self.stage} stage')
|
||||
self.proposal.send_admin_email('admin_payout')
|
||||
self.stage = MilestoneStage.ACCEPTED
|
||||
self.date_accepted = datetime.datetime.now()
|
||||
self.accept_arbiter_id = arbiter_id
|
||||
|
|
|
@ -22,7 +22,7 @@ from grant.utils.enums import (
|
|||
MilestoneStage
|
||||
)
|
||||
from grant.utils.exceptions import ValidationException
|
||||
from grant.utils.misc import dt_to_unix, make_url, gen_random_id
|
||||
from grant.utils.misc import dt_to_unix, make_url, make_admin_url, gen_random_id
|
||||
from grant.utils.requests import blockchain_get
|
||||
from grant.utils.stubs import anonymous_user
|
||||
|
||||
|
@ -462,6 +462,16 @@ class Proposal(db.Model):
|
|||
|
||||
return contribution
|
||||
|
||||
def send_admin_email(self, type: str):
|
||||
from grant.user.models import User
|
||||
admins = User.get_admins()
|
||||
for a in admins:
|
||||
send_email(a.email_address, type, {
|
||||
'user': a,
|
||||
'proposal': self,
|
||||
'proposal_url': make_admin_url(f'/proposals/{self.id}'),
|
||||
})
|
||||
|
||||
# state: status (DRAFT || REJECTED) -> (PENDING || STAKING)
|
||||
def submit_for_approval(self):
|
||||
self.validate_publishable()
|
||||
|
@ -485,6 +495,7 @@ class Proposal(db.Model):
|
|||
raise ValidationException(f"Proposal status must be staking in order to be set to pending")
|
||||
if not self.is_staked:
|
||||
raise ValidationException(f"Proposal is not fully staked, cannot set to pending")
|
||||
self.send_admin_email('admin_approval')
|
||||
self.status = ProposalStatus.PENDING
|
||||
db.session.add(self)
|
||||
db.session.flush()
|
||||
|
@ -543,6 +554,7 @@ class Proposal(db.Model):
|
|||
raise ValidationException(f"Proposal stage must be funding_required in order transition to funded state")
|
||||
if not self.is_funded:
|
||||
raise ValidationException(f"Proposal is not fully funded, cannot set to funded state")
|
||||
self.send_admin_email('admin_arbiter')
|
||||
self.stage = ProposalStage.WIP
|
||||
db.session.add(self)
|
||||
db.session.flush()
|
||||
|
|
|
@ -15,6 +15,7 @@ env.read_env()
|
|||
ENV = env.str("FLASK_ENV", default="production")
|
||||
DEBUG = ENV == "development"
|
||||
SITE_URL = env.str('SITE_URL', default='https://zfnd.org')
|
||||
ADMIN_SITE_URL = env.str('ADMIN_SITE_URL', default='https://grants-admin.zfnd.org')
|
||||
E2E_TESTING = env.str("E2E_TESTING", default=None)
|
||||
E2E_DATABASE_URL = env.str("E2E_DATABASE_URL", default=None)
|
||||
SQLALCHEMY_DATABASE_URI = E2E_DATABASE_URL if E2E_TESTING else env.str("DATABASE_URL")
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<p style="margin: 0 0 20px;">
|
||||
<a href="{{ args.proposal_url }}" target="_blank">
|
||||
{{ args.proposal.title }}</a
|
||||
>
|
||||
is awaiting approval. As an admin you can help out by reviewing it.
|
||||
</p>
|
||||
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
style="border-radius: 3px;"
|
||||
bgcolor="{{ UI.PRIMARY }}"
|
||||
>
|
||||
<a
|
||||
href="{{ args.proposal_url }}"
|
||||
target="_blank"
|
||||
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid {{
|
||||
UI.PRIMARY
|
||||
}}; display: inline-block;"
|
||||
>
|
||||
Review Proposal
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
|
@ -0,0 +1,3 @@
|
|||
{{ args.proposal.title }} is awaiting approval. As an admin you can help out by reviewing it.
|
||||
|
||||
Visit the proposal and review: {{ args.proposal_url }}
|
|
@ -0,0 +1,32 @@
|
|||
<p style="margin: 0 0 20px;">
|
||||
<a href="{{ args.proposal_url }}" target="_blank">
|
||||
{{ args.proposal.title }}</a
|
||||
>
|
||||
needs an arbiter. As an admin you can help out by nominating one.
|
||||
</p>
|
||||
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
style="border-radius: 3px;"
|
||||
bgcolor="{{ UI.PRIMARY }}"
|
||||
>
|
||||
<a
|
||||
href="{{ args.proposal_url }}"
|
||||
target="_blank"
|
||||
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid {{
|
||||
UI.PRIMARY
|
||||
}}; display: inline-block;"
|
||||
>
|
||||
Nominate Arbiter
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
|
@ -0,0 +1,3 @@
|
|||
{{ args.proposal.title }} needs an arbiter. As an admin you can help out by nominating one.
|
||||
|
||||
Visit the proposal and nominate an arbiter: {{ args.proposal_url }}
|
|
@ -0,0 +1,33 @@
|
|||
<p style="margin: 0 0 20px;">
|
||||
<a href="{{ args.proposal_url }}" target="_blank">
|
||||
{{ args.proposal.title }}</a
|
||||
>
|
||||
has an approved payout. As an admin you can help out by paying the milestone
|
||||
and marking it paid.
|
||||
</p>
|
||||
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td
|
||||
align="center"
|
||||
style="border-radius: 3px;"
|
||||
bgcolor="{{ UI.PRIMARY }}"
|
||||
>
|
||||
<a
|
||||
href="{{ args.proposal_url }}"
|
||||
target="_blank"
|
||||
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid {{
|
||||
UI.PRIMARY
|
||||
}}; display: inline-block;"
|
||||
>
|
||||
Pay Milestone
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
|
@ -0,0 +1,4 @@
|
|||
{{ args.proposal.title }} has an approved payout. As an admin you can help out by
|
||||
paying the milestone and marking it paid.
|
||||
|
||||
Pay the milestone: {{ args.proposal_url }}
|
|
@ -198,6 +198,10 @@ class User(db.Model, UserMixin):
|
|||
def get_by_email(email_address: str):
|
||||
return security.datastore.get_user(email_address)
|
||||
|
||||
@staticmethod
|
||||
def get_admins():
|
||||
return User.query.filter(User.is_admin == True).all()
|
||||
|
||||
def check_password(self, password: str):
|
||||
return verify_and_update_password(password, self)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import re
|
|||
import string
|
||||
import time
|
||||
|
||||
from grant.settings import SITE_URL, EXPLORER_URL
|
||||
from grant.settings import SITE_URL, ADMIN_SITE_URL, EXPLORER_URL
|
||||
|
||||
epoch = datetime.datetime.utcfromtimestamp(0)
|
||||
RANDOM_CHARS = string.ascii_letters + string.digits
|
||||
|
@ -37,6 +37,10 @@ def make_url(path: str):
|
|||
return f'{SITE_URL}{path}'
|
||||
|
||||
|
||||
def make_admin_url(path: str):
|
||||
return f'{ADMIN_SITE_URL}{path}'
|
||||
|
||||
|
||||
def make_explore_url(txid: str):
|
||||
return EXPLORER_URL.replace('<txid>', txid)
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ interface OwnProps {
|
|||
emailSubscriptions: EmailSubscriptions;
|
||||
loading: boolean;
|
||||
onSubmit: (settings: EmailSubscriptions) => void;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
type Props = OwnProps & FormComponentProps;
|
||||
|
@ -18,9 +19,9 @@ class EmailSubscriptionsForm extends React.Component<Props, {}> {
|
|||
this.props.form.setFieldsValue(this.props.emailSubscriptions);
|
||||
}
|
||||
render() {
|
||||
const { emailSubscriptions, loading } = this.props;
|
||||
const { emailSubscriptions, loading, isAdmin } = this.props;
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
const groupedSubs = groupEmailSubscriptionsByCategory(emailSubscriptions);
|
||||
const groupedSubs = groupEmailSubscriptionsByCategory(emailSubscriptions, isAdmin);
|
||||
|
||||
const fields = Object.entries(this.props.form.getFieldsValue());
|
||||
const numChecked = fields.map(([_, v]) => v).filter(v => v).length;
|
||||
|
|
|
@ -45,6 +45,7 @@ class EmailSubscriptions extends React.Component<Props, State> {
|
|||
emailSubscriptions={emailSubscriptions}
|
||||
loading={loading}
|
||||
onSubmit={this.setSubscriptions}
|
||||
isAdmin={!!authUser.isAdmin}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -62,6 +63,15 @@ class EmailSubscriptions extends React.Component<Props, State> {
|
|||
private setSubscriptions = (emailSubscriptions: IEmailSubscriptions) => {
|
||||
const { authUser } = this.props;
|
||||
if (!authUser) return;
|
||||
if (!authUser.isAdmin) {
|
||||
// fill in api-required fields which are not populated for non-admin form
|
||||
emailSubscriptions = {
|
||||
adminApproval: true,
|
||||
adminArbiter: true,
|
||||
adminPayout: true,
|
||||
...emailSubscriptions,
|
||||
};
|
||||
}
|
||||
this.setState({ loading: true });
|
||||
updateUserSettings(authUser.userid, { emailSubscriptions })
|
||||
.then(res => {
|
||||
|
|
|
@ -72,6 +72,23 @@ export const EMAIL_SUBSCRIPTIONS: { [key in ESKey]: EmailSubscriptionInfo } = {
|
|||
category: EMAIL_SUBSCRIPTION_CATEGORY.PROPOSAL,
|
||||
value: false,
|
||||
},
|
||||
|
||||
// ADMIN
|
||||
adminApproval: {
|
||||
description: 'proposal needs review',
|
||||
category: EMAIL_SUBSCRIPTION_CATEGORY.ADMIN,
|
||||
value: false,
|
||||
},
|
||||
adminArbiter: {
|
||||
description: 'proposal needs arbiter',
|
||||
category: EMAIL_SUBSCRIPTION_CATEGORY.ADMIN,
|
||||
value: false,
|
||||
},
|
||||
adminPayout: {
|
||||
description: 'milestone needs payout',
|
||||
category: EMAIL_SUBSCRIPTION_CATEGORY.ADMIN,
|
||||
value: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const EMAIL_SUBSCRIPTION_CATEGORIES: {
|
||||
|
@ -82,10 +99,18 @@ export const EMAIL_SUBSCRIPTION_CATEGORIES: {
|
|||
[EMAIL_SUBSCRIPTION_CATEGORY.FUNDED]: {
|
||||
description: 'Proposals you have contributed to',
|
||||
},
|
||||
[EMAIL_SUBSCRIPTION_CATEGORY.ADMIN]: { description: 'Admin' },
|
||||
};
|
||||
|
||||
export const groupEmailSubscriptionsByCategory = (es: EmailSubscriptions) => {
|
||||
return Object.entries(EMAIL_SUBSCRIPTION_CATEGORIES).map(([k, v]) => {
|
||||
export const groupEmailSubscriptionsByCategory = (
|
||||
es: EmailSubscriptions,
|
||||
withAdmin: boolean,
|
||||
) => {
|
||||
const catsForUser = { ...EMAIL_SUBSCRIPTION_CATEGORIES };
|
||||
if (!withAdmin) {
|
||||
delete catsForUser.ADMIN;
|
||||
}
|
||||
return Object.entries(catsForUser).map(([k, v]) => {
|
||||
const subscriptionSettings = Object.entries(EMAIL_SUBSCRIPTIONS)
|
||||
.filter(([_, sv]) => sv.category === k)
|
||||
.map(([sk, sv]) => {
|
||||
|
|
|
@ -12,12 +12,16 @@ export interface EmailSubscriptions {
|
|||
myProposalFunded: boolean;
|
||||
myProposalRefund: boolean;
|
||||
arbiter: boolean;
|
||||
adminApproval: boolean;
|
||||
adminArbiter: boolean;
|
||||
adminPayout: boolean;
|
||||
}
|
||||
|
||||
export enum EMAIL_SUBSCRIPTION_CATEGORY {
|
||||
GENERAL = 'GENERAL',
|
||||
PROPOSAL = 'PROPOSAL',
|
||||
FUNDED = 'FUNDED',
|
||||
ADMIN = 'ADMIN',
|
||||
}
|
||||
|
||||
export interface EmailSubscriptionInfo {
|
||||
|
|
|
@ -9,6 +9,7 @@ export interface User {
|
|||
title: string;
|
||||
socialMedias: SocialMedia[];
|
||||
avatar: { imageUrl: string } | null;
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
export interface UserSettings {
|
||||
|
|
Loading…
Reference in New Issue