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 %} + +
+ + +
+ {{ super() }} +
+{% endblock %} + +{% block scripts %} + +{{ super() }} +{% endblock %} + {% block content %}

-{{ ('Free as in money.') }} + {{ _('Free as in money.') }}

@@ -32,7 +52,8 @@ if user.participant: {% for i, rec in enumerate(packages_for_claiming, start=1) %} -
+
diff --git a/www/~/%username/emails/modify.json.spt b/www/~/%username/emails/modify.json.spt index 9b75a54e4d..b029c236b7 100644 --- a/www/~/%username/emails/modify.json.spt +++ b/www/~/%username/emails/modify.json.spt @@ -4,7 +4,6 @@ Manages the authenticated user's email addresses. import re from aspen import Response -from gratipay.exceptions import EmailTaken, EmailAlreadyVerified, Throttled from gratipay.utils import get_participant from gratipay.models.package import Package @@ -18,6 +17,7 @@ participant = get_participant(state, restrict=True) action = request.body['action'] address = request.body['address'] +show_address_in_message = bool(request.body.get('show_address_in_message', '')) # Basic checks. The real validation will happen when we send the email. if (len(address) > 254) or not email_re.match(address): @@ -44,7 +44,10 @@ if action in ('add-email', 'resend', 'start-verification'): raise Response(400) participant.start_email_verification(address, *packages) - msg = _("Check your inbox for a verification link.") + if show_address_in_message: + msg = _("Check {email_address} for a verification link.", email_address=address) + else: + msg = _("Check your inbox for a verification link.") elif action == 'set-primary': participant.set_primary_email(address) elif action == 'remove':