parent
16f16e8a15
commit
10efa96009
12 changed files with 94 additions and 23 deletions
|
@ -288,6 +288,7 @@ class User(BaseModel):
|
||||||
last_invalid_login = DateTimeField(default=datetime.utcnow)
|
last_invalid_login = DateTimeField(default=datetime.utcnow)
|
||||||
removed_tag_expiration_s = IntegerField(default=1209600) # Two weeks
|
removed_tag_expiration_s = IntegerField(default=1209600) # Two weeks
|
||||||
enabled = BooleanField(default=True)
|
enabled = BooleanField(default=True)
|
||||||
|
invoice_email_address = CharField(null=True, index=True)
|
||||||
|
|
||||||
def delete_instance(self, recursive=False, delete_nullable=False):
|
def delete_instance(self, recursive=False, delete_nullable=False):
|
||||||
# If we are deleting a robot account, only execute the subset of queries necessary.
|
# If we are deleting a robot account, only execute the subset of queries necessary.
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
"""Add invoice email address to user
|
||||||
|
|
||||||
|
Revision ID: 471caec2cb66
|
||||||
|
Revises: 88e0f440a2f
|
||||||
|
Create Date: 2015-12-28 13:57:17.761334
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '471caec2cb66'
|
||||||
|
down_revision = '88e0f440a2f'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
def upgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('user', sa.Column('invoice_email_address', sa.String(length=255), nullable=True))
|
||||||
|
op.create_index('user_invoice_email_address', 'user', ['invoice_email_address'], unique=False)
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(tables):
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('user', 'invoice_email_address')
|
||||||
|
### end Alembic commands ###
|
|
@ -125,7 +125,13 @@ def change_username(user_id, new_username):
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def change_invoice_email(user, invoice_email):
|
def change_invoice_email_address(user, invoice_email_address):
|
||||||
|
# Note: We null out the address if it is an empty string.
|
||||||
|
user.invoice_email_address = invoice_email_address or None
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
|
def change_send_invoice_email(user, invoice_email):
|
||||||
user.invoice_email = invoice_email
|
user.invoice_email = invoice_email
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ def org_view(o, teams):
|
||||||
|
|
||||||
if is_admin:
|
if is_admin:
|
||||||
view['invoice_email'] = o.invoice_email
|
view['invoice_email'] = o.invoice_email
|
||||||
|
view['invoice_email_address'] = o.invoice_email_address
|
||||||
|
|
||||||
return view
|
return view
|
||||||
|
|
||||||
|
@ -119,6 +120,10 @@ class Organization(ApiResource):
|
||||||
'type': 'boolean',
|
'type': 'boolean',
|
||||||
'description': 'Whether the organization desires to receive emails for invoices',
|
'description': 'Whether the organization desires to receive emails for invoices',
|
||||||
},
|
},
|
||||||
|
'invoice_email_address': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The email address at which to receive invoices',
|
||||||
|
},
|
||||||
'tag_expiration': {
|
'tag_expiration': {
|
||||||
'type': 'integer',
|
'type': 'integer',
|
||||||
'maximum': 2592000,
|
'maximum': 2592000,
|
||||||
|
@ -159,7 +164,13 @@ class Organization(ApiResource):
|
||||||
org_data = request.get_json()
|
org_data = request.get_json()
|
||||||
if 'invoice_email' in org_data:
|
if 'invoice_email' in org_data:
|
||||||
logger.debug('Changing invoice_email for organization: %s', org.username)
|
logger.debug('Changing invoice_email for organization: %s', org.username)
|
||||||
model.user.change_invoice_email(org, org_data['invoice_email'])
|
model.user.change_send_invoice_email(org, org_data['invoice_email'])
|
||||||
|
|
||||||
|
if ('invoice_email_address' in org_data and
|
||||||
|
org_data['invoice_email_address'] != org.invoice_email_address):
|
||||||
|
new_email = org_data['invoice_email_address']
|
||||||
|
logger.debug('Changing invoice email address for organization: %s', org.username)
|
||||||
|
model.user.change_invoice_email_address(org, new_email)
|
||||||
|
|
||||||
if 'email' in org_data and org_data['email'] != org.email:
|
if 'email' in org_data and org_data['email'] != org.email:
|
||||||
new_email = org_data['email']
|
new_email = org_data['email']
|
||||||
|
|
|
@ -109,6 +109,7 @@ def user_view(user):
|
||||||
'email': user.email,
|
'email': user.email,
|
||||||
'logins': [login_view(login) for login in logins],
|
'logins': [login_view(login) for login in logins],
|
||||||
'invoice_email': user.invoice_email,
|
'invoice_email': user.invoice_email,
|
||||||
|
'invoice_email_address': user.invoice_email_address,
|
||||||
'preferred_namespace': not (user.stripe_id is None),
|
'preferred_namespace': not (user.stripe_id is None),
|
||||||
'tag_expiration': user.removed_tag_expiration_s,
|
'tag_expiration': user.removed_tag_expiration_s,
|
||||||
})
|
})
|
||||||
|
@ -195,6 +196,10 @@ class User(ApiResource):
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': 'The user\'s username',
|
'description': 'The user\'s username',
|
||||||
},
|
},
|
||||||
|
'invoice_email_address': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Custom email address for receiving invoices',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'UserView': {
|
'UserView': {
|
||||||
|
@ -283,12 +288,16 @@ class User(ApiResource):
|
||||||
|
|
||||||
if 'invoice_email' in user_data:
|
if 'invoice_email' in user_data:
|
||||||
logger.debug('Changing invoice_email for user: %s', user.username)
|
logger.debug('Changing invoice_email for user: %s', user.username)
|
||||||
model.user.change_invoice_email(user, user_data['invoice_email'])
|
model.user.change_send_invoice_email(user, user_data['invoice_email'])
|
||||||
|
|
||||||
if 'tag_expiration' in user_data:
|
if 'tag_expiration' in user_data:
|
||||||
logger.debug('Changing user tag expiration to: %ss', user_data['tag_expiration'])
|
logger.debug('Changing user tag expiration to: %ss', user_data['tag_expiration'])
|
||||||
model.user.change_user_tag_expiration(user, user_data['tag_expiration'])
|
model.user.change_user_tag_expiration(user, user_data['tag_expiration'])
|
||||||
|
|
||||||
|
if ('invoice_email_address' in user_data and
|
||||||
|
user_data['invoice_email_address'] != user.invoice_email_address):
|
||||||
|
model.user.change_invoice_email_address(user, user_data['invoice_email_address'])
|
||||||
|
|
||||||
if 'email' in user_data and user_data['email'] != user.email:
|
if 'email' in user_data and user_data['email'] != user.email:
|
||||||
new_email = user_data['email']
|
new_email = user_data['email']
|
||||||
if model.user.find_user_by_email(new_email):
|
if model.user.find_user_by_email(new_email):
|
||||||
|
|
|
@ -37,7 +37,7 @@ def stripe_webhook():
|
||||||
invoice = stripe.Invoice.retrieve(invoice_id)
|
invoice = stripe.Invoice.retrieve(invoice_id)
|
||||||
if invoice:
|
if invoice:
|
||||||
invoice_html = renderInvoiceToHtml(invoice, user)
|
invoice_html = renderInvoiceToHtml(invoice, user)
|
||||||
send_invoice_email(user.email, invoice_html)
|
send_invoice_email(user.invoice_email_address or user.email, invoice_html)
|
||||||
|
|
||||||
elif event_type.startswith('customer.subscription.'):
|
elif event_type.startswith('customer.subscription.'):
|
||||||
cust_email = user.email if user is not None else 'unknown@domain.com'
|
cust_email = user.email if user is not None else 'unknown@domain.com'
|
||||||
|
|
15
static/css/directives/ui/billing-options.css
Normal file
15
static/css/directives/ui/billing-options.css
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
.billing-options .settings-option {
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-options .settings-option label {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.billing-options .settings-option .settings-description {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #888;
|
||||||
|
padding-left: 26px;
|
||||||
|
}
|
|
@ -506,21 +506,6 @@ i.toggle-icon:hover {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-option {
|
|
||||||
padding: 4px;
|
|
||||||
font-size: 18px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-option label {
|
|
||||||
margin-left: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-option .settings-description {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.organization-header-element {
|
.organization-header-element {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<!-- Options -->
|
<!-- Options -->
|
||||||
<div style="margin-bottom: 20px">
|
<div style="margin-bottom: 20px">
|
||||||
<div class="panel-title">
|
<div class="panel-title">
|
||||||
Billing Options
|
Billing Receipts
|
||||||
<div class="cor-loader-inline" ng-show="working"></div>
|
<div class="cor-loader-inline" ng-show="working"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
<input id="invoiceEmail" type="checkbox" ng-model="invoice_email">
|
<input id="invoiceEmail" type="checkbox" ng-model="invoice_email">
|
||||||
<label for="invoiceEmail">Send Receipt Emails</label>
|
<label for="invoiceEmail">Send Receipt Emails</label>
|
||||||
<div class="settings-description">
|
<div class="settings-description">
|
||||||
If checked, a receipt email will be sent to {{ obj.email }} on every successful charge
|
If checked, a receipt email will be sent to <a href="javascript:void(0)" ng-click="changeInvoiceEmailAddress()">{{ obj.invoice_email_address || obj.email }}</a> on every successful charge
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -34,6 +34,23 @@ angular.module('quay').directive('billingOptions', function () {
|
||||||
return difference < (60 * 60 * 24 * 60 * 1000 /* 60 days */);
|
return difference < (60 * 60 * 24 * 60 * 1000 /* 60 days */);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.changeInvoiceEmailAddress = function() {
|
||||||
|
bootbox.prompt('Enter the email address for receiving receipts', function(email) {
|
||||||
|
// Copied from Angular.
|
||||||
|
var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/i;
|
||||||
|
if (!email || !EMAIL_REGEXP.test(email)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.obj['invoice_email_address'] = email;
|
||||||
|
|
||||||
|
var errorHandler = ApiService.errorDisplay('Could not change user details');
|
||||||
|
ApiService.changeDetails($scope.organization, $scope.obj).then(function(resp) {
|
||||||
|
$scope.working = false;
|
||||||
|
}, errorHandler);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.changeCard = function() {
|
$scope.changeCard = function() {
|
||||||
var previousCard = $scope.currentCard;
|
var previousCard = $scope.currentCard;
|
||||||
$scope.changingCard = true;
|
$scope.changingCard = true;
|
||||||
|
|
Binary file not shown.
|
@ -26,8 +26,8 @@ def sendInvoice(invoice_id):
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
invoice_html = renderInvoiceToHtml(invoice, user)
|
invoice_html = renderInvoiceToHtml(invoice, user)
|
||||||
send_invoice_email(user.email, invoice_html)
|
send_invoice_email(user.invoice_email_address or user.email, invoice_html)
|
||||||
print 'Invoice sent to %s' % (user.email)
|
print 'Invoice sent to %s' % (user.invoice_email_address or user.email)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Email an invoice')
|
parser = argparse.ArgumentParser(description='Email an invoice')
|
||||||
parser.add_argument('invoice_id', help='The invoice ID')
|
parser.add_argument('invoice_id', help='The invoice ID')
|
||||||
|
|
Reference in a new issue