diff --git a/gratipay/testing/browser.py b/gratipay/testing/browser.py
index f62dff3e55..ae4da4c434 100644
--- a/gratipay/testing/browser.py
+++ b/gratipay/testing/browser.py
@@ -117,12 +117,12 @@ def wait_for_notification(self, type='notice'):
"""Wait for a certain ``type`` of notification. Dismiss the
notification and return the message.
"""
- n_selector = '.notifications-fixed .notification-{}'.format(type)
- m_selector = 'span.btn-close'
- notification = self.wait_for(n_selector).first
+ notification_selector = '.notifications-fixed .notification-{}'.format(type)
+ close_button_selector = 'span.btn-close'
+ notification = self.wait_for(notification_selector).first
message = notification.find_by_css('div').html
- notification.find_by_css(m_selector).first.click()
- self.wait_to_disappear(n_selector + ' ' + m_selector)
+ notification.find_by_css(close_button_selector).first.click()
+ self.wait_to_disappear('#{} {}'.format(notification['id'], close_button_selector))
return message
def wait_for_success(self):
diff --git a/js/gratipay/notification.js b/js/gratipay/notification.js
index cc74e14f0d..a2af1ba6e3 100644
--- a/js/gratipay/notification.js
+++ b/js/gratipay/notification.js
@@ -5,10 +5,12 @@
Gratipay.notification = function(text, type, timeout, closeCallback) {
var type = type || 'notice';
var timeout = timeout || (type == 'error' ? 10000 : 5000);
-
- var dialog = ['div', { 'class': 'notification notification-' + type }, [ 'div', text ]];
+ var id = Math.random().toString(36).substring(2, 100);
+ var placeholder = ['div', {'class': 'notification notification-' + type}, ['div', text]];
+ var dialog = ['div', {'class': 'notification notification-' + type, 'id': 'notification-'+id},
+ ['div', text]];
var $dialog = $([
- Gratipay.jsonml(dialog),
+ Gratipay.jsonml(placeholder),
Gratipay.jsonml(dialog)
]);
diff --git a/js/gratipay/packages.js b/js/gratipay/packages.js
index 67ad49d1c3..938ddc9cd1 100644
--- a/js/gratipay/packages.js
+++ b/js/gratipay/packages.js
@@ -1,33 +1,63 @@
Gratipay.packages = {};
-Gratipay.packages.init = function() {
+Gratipay.packages.initBulk = function() {
+ $('button.apply').on('click', Gratipay.packages.postBulk);
+};
+
+Gratipay.packages.initSingle = function() {
Gratipay.Select('.gratipay-select');
- $('button.apply').on('click', Gratipay.packages.post);
+ $('button.apply').on('click', Gratipay.packages.postOne);
};
-Gratipay.packages.post = function(e) {
+
+Gratipay.packages.postBulk = function(e) {
+ e.preventDefault();
+ var pkg, email, package_id, emails=[], package_ids_by_email={};
+ $('table.listing td.item ').not('.disabled').each(function() {
+ pkg = $(this).data();
+ if (package_ids_by_email[pkg.email] === undefined) {
+ emails.push(pkg.email);
+ package_ids_by_email[pkg.email] = [];
+ }
+ package_ids_by_email[pkg.email].push(pkg.packageId);
+ });
+ emails.sort();
+ for (var i=0, email; email = emails[i]; i++)
+ Gratipay.packages.post(email, package_ids_by_email[email], true);
+};
+
+Gratipay.packages.postOne = function(e) {
e.preventDefault();
- var $this = $(this);
- var action = 'start-verification';
- var package_id = $('input[name=package_id]').val();
var email = $('input[name=email]:checked').val();
+ var package_id = $('input[name=package_id]').val();
+ Gratipay.packages.post(email, [package_id]);
+}
+
- var $inputs = $('input, button');
- $inputs.prop('disabled', true);
+Gratipay.packages.post = function(email, package_ids, show_email) {
+ var action = 'start-verification';
+ var $button = $('.important-button button')
+ $button.prop('disabled', true);
+ function reenable() { $button.prop('disabled', false); }
$.ajax({
url: '/~' + Gratipay.username + '/emails/modify.json',
type: 'POST',
- data: {action: action, address: email, package_id: package_id},
+ data: { action: action
+ , address: email
+ , package_id: package_ids
+ , show_address_in_message: true
+ },
+ traditional: true,
dataType: 'json',
success: function (msg) {
if (msg) {
Gratipay.notification(msg, 'success');
+ reenable();
}
- $inputs.prop('disabled', false);
},
error: [
- function () { $inputs.prop('disabled', false); },
+ reenable,
Gratipay.error
],
});
diff --git a/tests/ttw/test_package_claiming.py b/tests/ttw/test_package_claiming.py
index e7261abcc5..002d0700fd 100644
--- a/tests/ttw/test_package_claiming.py
+++ b/tests/ttw/test_package_claiming.py
@@ -16,7 +16,8 @@ def check(self, choice=0):
self.css('label')[0].click() # activate select
self.css('label')[choice].click()
self.css('button')[0].click()
- assert self.wait_for_success() == 'Check your inbox for a verification link.'
+ address = ('alice' if choice == 0 else 'bob') + '@example.com'
+ assert self.wait_for_success() == 'Check {} for a verification link.'.format(address)
return self.db.one('select address from claims c join emails e on c.nonce = e.nonce')
def finish_claiming(self):
@@ -158,23 +159,23 @@ def setUp(self):
, emails=['alice@example.com', 'bob@example.com', 'cat@example.com']
)
+ def visit_as(self, username):
+ self.visit('/')
+ self.sign_in(username)
+ self.visit('/on/npm/')
+
def test_anon_gets_sign_in_prompt(self):
self.visit('/on/npm/')
assert self.css('.important-button button').text == 'Sign in / Sign up'
def test_auth_without_email_gets_highlighted_link_to_email(self):
self.make_participant('alice', claimed_time='now')
- self.visit('/')
- self.sign_in('alice')
- self.visit('/on/npm/')
+ self.visit_as('alice')
assert self.css('.highlight').text == 'Link an email'
def test_auth_without_claimable_packages_gets_disabled_apply_button(self):
self.make_participant('doug', claimed_time='now', email_address='doug@example.com')
- self.visit('/')
- self.sign_in('doug')
- self.visit('/on/npm/')
-
+ self.visit_as('doug')
button = self.css('.important-button button')
assert button.text == 'Apply to accept payments'
assert button['disabled'] == 'true'
@@ -183,10 +184,7 @@ def test_auth_with_claimable_packages_gets_apply_button(self):
alice = self.make_participant('alice', claimed_time='now',
email_address='alice@example.com')
self.add_and_verify_email(alice, 'bob@example.com')
- self.visit('/')
- self.sign_in('alice')
- self.visit('/on/npm/')
-
+ self.visit_as('alice')
button = self.css('.important-button button')
assert button.text == 'Apply to accept payments'
assert button['disabled'] is None
@@ -197,12 +195,43 @@ def test_differentiates_claimed_packages(self):
email_address='alice@example.com')
Package.from_names(NPM, 'foo').get_or_create_linked_team(self.db, alice)
Package.from_names(NPM, 'bar').get_or_create_linked_team(self.db, bob)
- self.visit('/')
- self.sign_in('alice')
- self.visit('/on/npm/')
-
+ self.visit_as('alice')
assert self.css('.i1').has_class('disabled')
assert self.css('.i1 .owner a').text == '~bob'
assert not self.css('.i2').has_class('disabled')
assert self.css('.i3').has_class('disabled')
assert self.css('.i3 .owner a').text == 'you'
+
+ def test_sends_mail(self):
+ self.make_participant('cat', claimed_time='now', email_address='cat@example.com')
+ self.visit_as('cat')
+ self.css('.important-button button').click()
+ assert self.wait_for_success() == 'Check cat@example.com for a verification link.'
+
+ def test_sends_one_mail_per_address(self):
+ cat = self.make_participant('cat', claimed_time='now', email_address='cat@example.com')
+ self.add_and_verify_email(cat, 'bob@example.com')
+ self.visit_as('cat')
+ self.css('.important-button button').click()
+ assert self.wait_for_success() == 'Check bob@example.com for a verification link.'
+ assert self.wait_for_success() == 'Check cat@example.com for a verification link.'
+
+ def test_sends_one_mail_for_multiple_packages(self):
+ self.make_participant('alice', claimed_time='now', email_address='alice@example.com')
+ self.visit_as('alice')
+ self.css('.important-button button').click()
+ assert len(self.css('table.listing td.item')) == 3
+ assert self.wait_for_success() == 'Check alice@example.com for a verification link.'
+ assert self.db.one('select count(*) from claims') == 3
+ assert self.db.one('select count(*) from email_queue') == 1
+
+ def test_doesnt_send_for_unclaimable_packages(self):
+ self.make_participant('alice', claimed_time='now', email_address='alice@example.com')
+ cat = self.make_participant('cat', claimed_time='now', email_address='cat@example.com')
+ Package.from_names(NPM, 'baz').get_or_create_linked_team(self.db, cat)
+ self.visit_as('alice')
+ self.css('.important-button button').click()
+ assert len(self.css('table.listing td.item')) == 3
+ assert self.wait_for_success() == 'Check alice@example.com for a verification link.'
+ assert self.db.one('select count(*) from claims') == 2
+ assert self.db.one('select count(*) from email_queue') == 1
diff --git a/www/on/npm/%package/index.html.spt b/www/on/npm/%package/index.html.spt
index e969e37cf2..9efb9543bb 100644
--- a/www/on/npm/%package/index.html.spt
+++ b/www/on/npm/%package/index.html.spt
@@ -37,7 +37,7 @@ if user.participant:
{% block scripts %}
{{ super() }}
diff --git a/www/on/npm/index.html.spt b/www/on/npm/index.html.spt
index f41c76207f..80fd43eb1c 100644
--- a/www/on/npm/index.html.spt
+++ b/www/on/npm/index.html.spt
@@ -10,10 +10,30 @@ if user.participant:
any_claimable = any([rec.claimed_by is None for rec in packages_for_claiming])
[---]
{% extends "templates/base.html" %}
+
+{% block banner %}
+
+
-{{ ('Free as in money.') }} + {{ _('Free as in money.') }}
@@ -32,7 +52,8 @@ if user.participant: