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

Commit

Permalink
Checkpoint 1
Browse files Browse the repository at this point in the history
  • Loading branch information
chadwhitacre committed Mar 17, 2017
1 parent c379ad7 commit 11a436f
Show file tree
Hide file tree
Showing 15 changed files with 236 additions and 19 deletions.
6 changes: 5 additions & 1 deletion gratipay/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@
from .community import Community
from .country import Country
from .exchange_route import ExchangeRoute
from .package import Package
from .participant import Participant
from .team import Team


MODELS = (AccountElsewhere, Community, Country, ExchangeRoute, Package, Participant, Team)


@contextmanager
def just_yield(obj):
yield obj
Expand All @@ -32,7 +36,7 @@ def __init__(self, app, *a, **kw):
``.app``.
"""
Postgres.__init__(self, *a, **kw)
for model in (AccountElsewhere, Community, Country, ExchangeRoute, Participant, Team):
for model in MODELS:
self.register_model(model)
model.app = app

Expand Down
49 changes: 49 additions & 0 deletions gratipay/models/package/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

from postgres.orm import Model


NPM = 'npm' # We are starting with a single package manager. If we see
# traction we will expand.


class Package(Model):
"""Represent a gratipackage. :-)
"""

typname = 'packages'

def __eq__(self, other):
if not isinstance(other, Package):
return False
return self.id == other.id

def __ne__(self, other):
if not isinstance(other, Package):
return True
return self.id != other.id


# Constructors
# ============

@classmethod
def from_id(cls, id):
"""Return an existing package based on id.
"""
return cls.db.one("SELECT packages.*::packages FROM packages WHERE id=%s", (id,))

@classmethod
def from_names(cls, package_manager, name):
"""Return an existing package based on package manager and package names.
"""
return cls.db.one("SELECT packages.*::packages FROM packages "
"WHERE package_manager=%s and name=%s", (package_manager, name))


# Emails
# ======

def send_confirmation_email(self, address):
pass
10 changes: 10 additions & 0 deletions gratipay/testing/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,16 @@ def make_team(self, *a, **kw):
return team


def make_package(self, package_manager='npm', name='foo', description='Foo',
emails=['[email protected]']):
"""Factory for packages.
"""
return self.db.one( 'INSERT INTO packages (package_manager, name, description, emails) '
'VALUES (%s, %s, %s, %s) RETURNING *'
, (package_manager, name, description, emails)
)


def make_participant(self, username, **kw):
"""Factory for :py:class:`~gratipay.models.participant.Participant`.
"""
Expand Down
1 change: 0 additions & 1 deletion js/gratipay/emails.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ Gratipay.emails.post = function(e) {
var $this = $(this);
var action = this.className;
var $inputs = $('.emails button, .emails input');
console.log($this);
var address = $this.parent().data('email') || $('input.add-email').val();

$inputs.prop('disabled', true);
Expand Down
29 changes: 29 additions & 0 deletions js/gratipay/packages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Gratipay.packages = {};

Gratipay.packages.post = function(e) {
e.preventDefault();
var $this = $(this);
var action = 'add-email-and-claim-package';
var package_id = $('input[name=package_id]').val();
var email = $('input[name=email]:checked').val();

var $inputs = $('input, button');
$inputs.prop('disabled', true);

$.ajax({
url: '/~' + Gratipay.username + '/emails/modify.json',
type: 'POST',
data: {action: action, address: email, package_id: package_id},
dataType: 'json',
success: function (msg) {
if (msg) {
Gratipay.notification(msg, 'success');
}
$inputs.prop('disabled', false);
},
error: [
function () { $inputs.prop('disabled', false); },
Gratipay.error
],
});
};
6 changes: 6 additions & 0 deletions scss/components/listing.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
.sorry {
text-align: center;
font: normal 12px/15px $Ideal;
color: $medium-gray;
}

table.listing {
width: 100%;

Expand Down
7 changes: 0 additions & 7 deletions scss/pages/on-npm-foo.scss

This file was deleted.

8 changes: 8 additions & 0 deletions scss/pages/package.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#package {
.emails {
margin: 1em 0;
li {
list-style: none;
}
}
}
6 changes: 0 additions & 6 deletions scss/pages/search.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@
}
}

.sorry {
text-align: center;
font: normal 12px/15px $Ideal;
color: $medium-gray;
}

h2 {
margin-top: 4em;
&:first-of-type {
Expand Down
16 changes: 16 additions & 0 deletions tests/py/test_packages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

from gratipay.models.package import NPM, Package
from gratipay.testing import Harness


class TestPackage(Harness):

def test_can_be_instantiated_from_id(self):
p = self.make_package()
assert Package.from_id(p.id).id == p.id

def test_can_be_instantiated_from_names(self):
self.make_package()
assert Package.from_names(NPM, 'foo').name == 'foo'
37 changes: 37 additions & 0 deletions tests/py/test_www_npm_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

from gratipay.models.package import NPM
from gratipay.testing import Harness


class TestClaimingWorkflow(Harness):

def setUp(self):
self.make_package()

def test_anon_gets_signin_page_from_unclaimed(self):
body = self.client.GET('/on/npm/foo/').body
assert 'npm/foo</a> has not been claimed' in body
assert 'with a couple clicks' in body

def test_auth_gets_send_confirmation_page_from_unclaimed(self):
self.make_participant('bob', claimed_time='now')
body = self.client.GET('/on/npm/foo/', auth_as='bob').body
assert 'npm/foo</a> has not been claimed' in body
assert 'using any email address' in body
assert '[email protected]' in body

def test_auth_gets_multiple_options_if_present(self):
self.make_package(NPM, 'bar', 'Bar', ['[email protected]', '[email protected]'])
self.make_participant('bob', claimed_time='now')
body = self.client.GET('/on/npm/bar/', auth_as='bob').body
assert 'using any email address' in body
assert '[email protected]' in body
assert '[email protected]' in body

def test_auth_gets_something_if_no_emails(self):
self.make_package(NPM, 'bar', 'Bar', [])
self.make_participant('bob', claimed_time='now')
body = self.client.GET('/on/npm/bar/', auth_as='bob').body
assert "didn&#39;t find any email addresses" in body
28 changes: 28 additions & 0 deletions tests/ttw/test_package_claiming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

from gratipay.testing import BrowserHarness


class TestSendConfirmationLink(BrowserHarness):

def check(self, choice=0):
self.make_participant('bob', claimed_time='now')
self.sign_in('bob')
self.visit('/on/npm/foo/')
self.css('input[type=radio]')[choice].click()
self.css('button')[0].click()
assert self.has_element('.notification.notification-success', 1)
assert self.has_text('Check [email protected] for a confirmation link.')

def test_appears_to_work(self):
self.make_package()
self.check()

def test_works_when_there_are_multiple_addresses(self):
self.make_package(emails=['[email protected]', '[email protected]'])
self.check()

def test_can_send_to_second_email(self):
self.make_package(emails=['[email protected]', '[email protected]'])
self.check(choice=1)
2 changes: 1 addition & 1 deletion www/assets/gratipay.css.spt
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@
@import "scss/pages/history";
@import "scss/pages/identities";
@import "scss/pages/team";
@import "scss/pages/package";
@import "scss/pages/profile-edit";
@import "scss/pages/giving";
@import "scss/pages/settings";
@import "scss/pages/on-confirm";
@import "scss/pages/on-npm-foo";
@import "scss/pages/search";
@import "scss/pages/hall-of-fame";
44 changes: 41 additions & 3 deletions www/on/npm/%package/index.html.spt
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import requests

from aspen import Response
from gratipay.utils import markdown
from gratipay.models.package import Package
[---]
package_name = request.path['package']
package = website.db.one("select * from packages where package_manager='npm' "
"and name=%s", (package_name,))
package = Package.from_names('npm', package_name)
if package is None:
raise Response(404)
banner = package_name
page_id = "on-npm-foo"
page_id = "package"
suppress_sidebar = True
url = 'https://npmjs.com/package/' + package.name
[---]
Expand All @@ -26,5 +26,43 @@ url = 'https://npmjs.com/package/' + package.name
</a>
{% endblock %}

{% block scripts %}
<script>
$(document).ready(function() {
$('button.send').on('click', Gratipay.packages.post);
});
</script>
{{ super() }}
{% endblock %}

{% block content %}
<h2>{{ _( '{npm_package} has not been claimed on Gratipay.'
, npm_package=('<a href="' + url + '">' + 'npm/' + package_name + '</a>')|safe
) }}</h2>
{% if user.ANON %}
<p>{{ _('Is this yours? You can claim it on Gratipay with a couple clicks:') }}</p>
{% include "templates/sign-in-using.html" %}

<h2>{{ _('What is Gratipay?') }}</h2>
<p>{{ _('Gratipay helps companies and others pay for open source.') }}
<a href="/about/">{{ _("Learn more") }}</a></p>
{% else %}
<p>{{ _( 'Is this yours? You can claim it on Gratipay using any email address {a}on file{_a} in the maintainers field in the npm registry.'
, a=('<a href="https://registry.npmjs.com/' + package.name + '">')|safe
, _a='</a>'|safe
) }}
{% if len(package.emails) == 0 %}
<p class="sorry">{{ _("Sorry, we didn't find any email addresses on file.") }}</p>
{% else %}
<input type="hidden" name="package_id" value="{{ package.id }}">
<ul class="emails">
{% for i, email in enumerate(package.emails) %}
<li><input type="radio" name="email" value="{{ email }}" id="email-{{i}}"
{% if i == 0 %}checked{% endif %}>
<label for="email-{{ i }}">{{ email }}</a></li>
{% endfor %}
</ul>
<button type="submit" class="send selected">{{ _('Send confirmation link') }}</button>
{% endif %}
{% endif %}
{% endblock %}
6 changes: 6 additions & 0 deletions www/~/%username/emails/modify.json.spt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import re
from aspen import Response
from gratipay.exceptions import EmailTaken, EmailAlreadyVerified
from gratipay.utils import get_participant
from gratipay.models.package import Package

# exactly one @, and at least one . after @
email_re = re.compile(r'^[^@]+@[^@]+\.[^@]+$')
Expand Down Expand Up @@ -43,6 +44,11 @@ elif action == 'set-primary':
participant.update_email(address)
elif action == 'remove':
participant.remove_email(address)
elif action == 'add-email-and-claim-package':
package_id = request.body['package_id']
package = Package.from_id(package_id)
package.send_confirmation_email(address)
msg = _("Check {email} for a confirmation link.", email=address)
else:
raise Response(400, 'unknown action "%s"' % action)

Expand Down

0 comments on commit 11a436f

Please sign in to comment.