Skip to content
This repository has been archived by the owner on Feb 8, 2018. It is now read-only.

Commit

Permalink
Implement bulk claiming on /on/npm/
Browse files Browse the repository at this point in the history
  • Loading branch information
chadwhitacre committed Jun 14, 2017
1 parent c550047 commit d9242ab
Show file tree
Hide file tree
Showing 11 changed files with 347 additions and 42 deletions.
12 changes: 12 additions & 0 deletions gratipay/testing/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,19 @@ def get_tip(self, tipper, tippee):
def add_and_verify_email(self, participant, *emails):
"""Given a participant and some email addresses, add and verify them.
"""
if participant.__class__ is not Participant:
participant = P(participant)
for email in emails:
participant.start_email_verification(email)
nonce = participant.get_email(email).nonce
participant.finish_email_verification(email, nonce)


def claim_package(self, participant, package):
"""Given a participant and a package, claim the package for the participant.
"""
if participant.__class__ is not Participant:
participant = P(participant)
if package.__class__ is not Package:
package = Package.from_names(NPM, package)
package.get_or_create_linked_team(self.db, participant)
49 changes: 38 additions & 11 deletions js/gratipay/packages.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,60 @@
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, package_ids_by_email={};
$('table.listing td.item ').not('.disabled').each(function() {
pkg = $(this).data();
if (package_ids_by_email[pkg.email] === undefined)
package_ids_by_email[pkg.email] = [];
package_ids_by_email[pkg.email].push(pkg.packageId);
});
for (email in package_ids_by_email)
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 = $('button.apply')

$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
],
});
Expand Down
18 changes: 18 additions & 0 deletions scss/components/listing.scss
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,24 @@ table.listing {
border-bottom: 1px solid $light-brown;
}
}

&.disabled {
img.avatar {
filter: grayscale(100%) brightness(110%);
}
.package-manager img {
filter: grayscale(100%) brightness(120%);
}
.listing-name {
color: $light-gray ! important;
}
.status a {
color: $medium-gray! important;
}
.owner a {
color: $medium-gray! important;
}
}
}
}
.with-sidebar table.listing td.item .listing-details {
Expand Down
12 changes: 8 additions & 4 deletions scss/components/text-treatments.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
}
}

.sorry {
.instructions {
text-align: center;
font: normal 12px/15px $Ideal;
color: $medium-gray;
margin: 0 0 30px;
font-family: $Ideal;
}
.note {

.note, .sorry, .fine-print {
font: normal 12px/15px $Ideal;
color: $medium-gray;
a {
Expand All @@ -29,6 +30,9 @@
color: $medium-gray;
}
}
.sorry, .fine-print {
text-align: center;
}
.listing-name {
color: $black;
font: bold 20px/24px $Ideal;
Expand Down
6 changes: 0 additions & 6 deletions scss/pages/package.scss
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
#package #content {
text-align: center;

.instructions {
text-align: center;
margin: 0 0 30px;
font-family: $Ideal;
}

.selected .icon { display: block; }
.icon {
display: none;
Expand Down
22 changes: 21 additions & 1 deletion tests/py/test_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ def test_package_verification_fails_if_email_not_listed(self):
assert response.code == 400
assert self.db.all('select package_id from claims order by package_id') == []

def test_package_verification_fails_package_id_is_garbage(self):
def test_package_verification_fails_if_package_id_is_garbage(self):
response = self.hit_email_spt( 'start-verification'
, '[email protected]'
, package_ids=['cheese monkey']
Expand All @@ -304,6 +304,26 @@ def test_package_verification_fails_package_id_is_garbage(self):
assert response.code == 400
assert self.db.all('select package_id from claims order by package_id') == []

def test_package_reverification_succeeds_if_package_is_already_claimed_by_self(self):
foo = self.make_package()
self.claim_package('alice', foo)
response = self.hit_email_spt( 'start-verification'
, '[email protected]'
, package_ids=[foo.id]
)
assert response.code == 200

def test_package_verification_fails_if_package_is_already_claimed_by_other(self):
self.make_participant('bob', claimed_time='now', email_address='[email protected]')
foo = self.make_package(emails=['[email protected]', '[email protected]'])
self.claim_package('bob', foo)
response = self.hit_email_spt( 'start-verification'
, '[email protected]'
, package_ids=[foo.id]
, should_fail=True
)
assert response.code == 400


class TestFunctions(Alice):

Expand Down
10 changes: 10 additions & 0 deletions tests/py/test_www_npm_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,13 @@ def test_package_redirects_to_project_if_claimed(self):
def test_package_served_as_project_if_claimed(self):
self.claim_package()
assert 'owned by' in self.client.GET('/foo/').body


class Bulk(Harness):

def setUp(self):
self.make_package()

def test_anon_gets_signin_page(self):
body = self.client.GET('/on/npm/').body
assert '0 out of all 1 npm package' in body
92 changes: 91 additions & 1 deletion tests/ttw/test_package_claiming.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -143,3 +144,92 @@ def test_jdorfman_can_merge_accounts(self):
payload = eval(self.css('table#events td.payload').text)
assert payload['action'] == 'take-over'
assert payload['values']['exchange_routes'] == [r.id for r in jdorfman.get_payout_routes()]


class BulkClaiming(BrowserHarness):

def setUp(self):
self.make_package()
self.make_package( name='bar'
, description='Bar barringly'
, emails=['[email protected]', '[email protected]']
)
self.make_package( name='baz'
, description='Baz bazzingly'
, emails=['[email protected]', '[email protected]', '[email protected]']
)

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_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='[email protected]')
self.visit_as('doug')
button = self.css('.important-button button')
assert button.text == 'Apply to accept payments'
assert button['disabled'] == 'true'

def test_auth_with_claimable_packages_gets_apply_button(self):
self.make_participant('alice', claimed_time='now', email_address='[email protected]')
self.add_and_verify_email('alice', '[email protected]')
self.visit_as('alice')
button = self.css('.important-button button')
assert button.text == 'Apply to accept payments'
assert button['disabled'] is None

def test_differentiates_claimed_packages(self):
self.make_participant('bob', claimed_time='now', email_address='[email protected]')
self.make_participant('alice', claimed_time='now', email_address='[email protected]')
self.claim_package('alice', 'foo')
self.claim_package('bob', 'bar')
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='[email protected]')
self.visit_as('cat')
self.css('.important-button button').click()
assert self.wait_for_success() == 'Check [email protected] for a verification link.'

def test_sends_one_mail_per_address(self):
cat = self.make_participant('cat', claimed_time='now', email_address='[email protected]')
self.add_and_verify_email(cat, '[email protected]')
self.visit_as('cat')
self.css('.important-button button').click()
assert self.wait_for_success('Check [email protected] for a verification link.')
assert self.wait_for_success('Check [email protected] for a verification link.')

def test_sends_one_mail_for_multiple_packages(self):
self.make_participant('alice', claimed_time='now', email_address='[email protected]')
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 [email protected] 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='[email protected]')
self.make_participant('cat', claimed_time='now', email_address='[email protected]')
self.claim_package('cat', 'baz')
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 [email protected] for a verification link.'
assert self.db.one('select count(*) from claims') == 2
assert self.db.one('select count(*) from email_queue') == 1
32 changes: 18 additions & 14 deletions www/on/npm/%package/index.html.spt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ if user.participant:
{% block scripts %}
<script>
$(document).ready(function() {
Gratipay.packages.init();
Gratipay.packages.initSingle();
});
</script>
{{ super() }}
Expand All @@ -48,22 +48,22 @@ if user.participant:
{% if package.description %}
<p class="description important-thing-at-the-top{% if len(package.description) > 256 %} long{% endif %}">{{ package.description }}</p>
{% else %}
<p class="note important-thing-at-the-top">{{ _("No description available.") }}</p>
<p class="sorry important-thing-at-the-top">{{ _("No description available.") }}</p>
{% endif %}

<p class="instructions">
{{ _( 'Apply to accept payments for the {package_link} npm package:'
, package_link=('<a href="' + package.remote_human_url + '">' + package_name + '</a>')|safe
) }}
</p>
</p>

{% if user.ANON %}
<div class="important-button">
{{ sign_in_using(button_class='large') }}
</div>
{% else %}
{% if len(emails) == 0 %}
<p class="note">{{ _("No email addresses on file.") }}</p>
<p class="sorry">{{ _("No email addresses on file.") }}</p>
{% else %}
<input type="hidden" name="package_id" value="{{ package.id }}">
<div class="gratipay-select">
Expand Down Expand Up @@ -115,15 +115,19 @@ if user.participant:
</button>
</div>
{% endif %}
<p class="note">{{ _( 'Addresses are from {a}{code}maintainers{_code}{_a}.'
, a=('<a href="' + package.remote_api_url + '">')|safe
, _a='</a>'|safe
, code='<code>'|safe
, _code='</code>'|safe
) }}</p>
<p class="note">{{ _( 'Out of date? Update {a}at npm{_a} and refresh.'
, a=('<a href="' + package.remote_human_url + '">')|safe
, _a='</a>'|safe
) }}</p>
<p class="fine-print">
{{ _( 'Addresses are from {a}{code}maintainers{_code}{_a}.'
, a=('<a href="' + package.remote_api_url + '">')|safe
, _a='</a>'|safe
, code='<code>'|safe
, _code='</code>'|safe
) }}
</p>
<p class="fine-print">
{{ _( 'Out of date? Update {a}at npm{_a} and refresh.'
, a=('<a href="' + package.remote_human_url + '">')|safe
, _a='</a>'|safe
) }}
</p>
{% endif %}
{% endblock %}
Loading

0 comments on commit d9242ab

Please sign in to comment.