diff --git a/emails/claim-package.spt b/emails/claim-package.spt
new file mode 100644
index 0000000000..cbbd9fdb57
--- /dev/null
+++ b/emails/claim-package.spt
@@ -0,0 +1,17 @@
+{{ _("Claim {0} on Gratipay?", project) }}
+
+[---] text/html
+{{ _("We've received a request to connect {0} to the {1} account on Gratipay. Sound familiar?",
+ ('%s'|safe) % email,
+ ('{0}'|safe).format(username)) }}
+
+
+{{ _("Yes, proceed!") }}
+
+[---] text/plain
+{{ _("We've received a request to connect {0} to the {1} account on Gratipay. Sound familiar?",
+ email, username) }}
+
+{{ _("Follow this link to finish connecting your email:") }}
+
+{{ link }}
diff --git a/gratipay/models/package/__init__.py b/gratipay/models/package/__init__.py
index 571e2cb971..24af56d724 100644
--- a/gratipay/models/package/__init__.py
+++ b/gratipay/models/package/__init__.py
@@ -40,10 +40,3 @@ def from_names(cls, package_manager, name):
"""
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
diff --git a/gratipay/models/participant/email.py b/gratipay/models/participant/email.py
index fac4aab392..891467a840 100644
--- a/gratipay/models/participant/email.py
+++ b/gratipay/models/participant/email.py
@@ -37,13 +37,20 @@ class Email(object):
"""
- def add_email(self, email, resend_threshold='3 minutes'):
+ def add_email(self, email, package=None, resend_threshold='3 minutes'):
"""Add an email address for a participant.
This is called when adding a new email address, and when resending the
verification email for an unverified email address.
- :returns: the number of emails sent.
+ :param unicode email: the email address to add
+ :param Package package: a package the participant is claiming
+ :param unicode resend_threshold: the time interval to wait before
+ sending another verification message
+
+ :returns: the number of emails sent
+
+ If ``package`` is provided, then
"""
@@ -57,6 +64,8 @@ def add_email(self, email, resend_threshold='3 minutes'):
""", locals())
if owner:
if owner == self.username:
+ if package:
+ return self.initiate_package_claim(package, email)
return 0
else:
raise EmailAlreadyTaken(email)
diff --git a/gratipay/models/participant/package_claiming.py b/gratipay/models/participant/package_claiming.py
new file mode 100644
index 0000000000..86a081d430
--- /dev/null
+++ b/gratipay/models/participant/package_claiming.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+
+class PackageClaiming(object):
+
+ """Gratipay participants may claim packages on the Node package manager
+ (npm), bringing them into Gratipay as projects similar to any other. The
+ claiming process is handled via email: ``initiate_package_claim`` sends an
+ email to an address registered with npm, and a link back from the email
+ lands in ``claim_package`` to finalize the claim.
+
+ Packages can also be unclaimed, and reclaimed.
+
+ """
+
+ def initiate_package_claim(self, package, email):
+ """Initiate a claim on the given package.
+
+ :param Package package: a ``Package`` instance
+
+ :returns: ``None``
+
+ """
+ assert email in package.emails # sanity check
+
+ r = self.send_email('claim_package',
+ email=email,
+ link=link.format(**locals()),
+ include_unsubscribe=False)
+ assert r == 1 # Make sure the verification email was sent
+ if self.email_address:
+ self.send_email('verification_notice',
+ new_email=email,
+ include_unsubscribe=False)
+ return 2
+ return 1
diff --git a/gratipay/testing/harness.py b/gratipay/testing/harness.py
index bf8da42fe2..bb5a23e8fc 100644
--- a/gratipay/testing/harness.py
+++ b/gratipay/testing/harness.py
@@ -19,6 +19,7 @@
from gratipay.models.account_elsewhere import AccountElsewhere
from gratipay.models.exchange_route import ExchangeRoute
from gratipay.models.participant import Participant
+from gratipay.models.package import NPM, Package
from gratipay.security import user
from gratipay.testing.vcr import use_cassette
from psycopg2 import IntegrityError, InternalError
@@ -181,14 +182,15 @@ def make_team(self, *a, **kw):
return team
- def make_package(self, package_manager='npm', name='foo', description='Foo',
+ def make_package(self, package_manager=NPM, name='foo', description='Foo',
emails=['alice@example.com']):
"""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)
- )
+ self.db.run( 'INSERT INTO packages (package_manager, name, description, emails) '
+ 'VALUES (%s, %s, %s, %s) RETURNING *'
+ , (package_manager, name, description, emails)
+ )
+ return Package.from_names(NPM, name)
def make_participant(self, username, **kw):
diff --git a/tests/py/test_email.py b/tests/py/test_email.py
index 291f1f4c2b..69e6679d9c 100644
--- a/tests/py/test_email.py
+++ b/tests/py/test_email.py
@@ -237,12 +237,12 @@ def test_cannot_resend_verification_too_frequently(self):
self.alice.add_email('alice@gratipay.coop')
time.sleep(0.05)
with self.assertRaises(ResendingTooFast):
- self.alice.add_email('alice@gratipay.coop', '0.1 seconds')
+ self.alice.add_email('alice@gratipay.coop', resend_threshold='0.1 seconds')
def test_can_resend_verification_after_a_while(self):
self.alice.add_email('alice@gratipay.coop')
time.sleep(0.15)
- self.alice.add_email('alice@gratipay.coop', '0.1 seconds')
+ self.alice.add_email('alice@gratipay.coop', resend_threshold='0.1 seconds')
def test_html_escaping(self):
self.alice.add_email("foo'bar@example.com")
diff --git a/tests/py/test_packages.py b/tests/py/test_packages.py
index b97e888377..c0b3611c97 100644
--- a/tests/py/test_packages.py
+++ b/tests/py/test_packages.py
@@ -3,6 +3,7 @@
from gratipay.models.package import NPM, Package
from gratipay.testing import Harness
+from gratipay.testing.emails import EmailHarness
class TestPackage(Harness):
@@ -14,3 +15,12 @@ def test_can_be_instantiated_from_id(self):
def test_can_be_instantiated_from_names(self):
self.make_package()
assert Package.from_names(NPM, 'foo').name == 'foo'
+
+
+class TestClaiming(EmailHarness):
+
+ def test_participant_can_initiate_package_claim(self):
+ alice = self.make_participant('alice', claimed_time='now')
+ p = self.make_package()
+ alice.initiate_package_claim(p)
+ assert self.get_last_email()
diff --git a/tests/py/test_take_over.py b/tests/py/test_take_over.py
index 1ba292c764..06f0b56f71 100644
--- a/tests/py/test_take_over.py
+++ b/tests/py/test_take_over.py
@@ -190,10 +190,10 @@ def test_email_addresses_merging(self):
alice.verify_email('alice@example.org', alice.get_email('alice@example.org').nonce)
bob_github = self.make_elsewhere('github', 2, 'bob')
bob = bob_github.opt_in('bob')[0].participant
- bob.add_email('alice@example.com', '0 seconds')
+ bob.add_email('alice@example.com', resend_threshold='0 seconds')
bob.verify_email('alice@example.com', bob.get_email('alice@example.com').nonce)
- bob.add_email('alice@example.net', '0 seconds')
- bob.add_email('bob@example.net', '0 seconds')
+ bob.add_email('alice@example.net', resend_threshold='0 seconds')
+ bob.add_email('bob@example.net', resend_threshold='0 seconds')
alice.take_over(bob_github, have_confirmation=True)
alice_emails = {e.address: e for e in alice.get_emails()}
diff --git a/www/~/%username/emails/modify.json.spt b/www/~/%username/emails/modify.json.spt
index 14ce8ac2e4..2ccb9719fa 100644
--- a/www/~/%username/emails/modify.json.spt
+++ b/www/~/%username/emails/modify.json.spt
@@ -28,7 +28,7 @@ if not participant.email_lang:
msg = None
if action in ('add-email', 'resend'):
- r = participant.add_email(address, website.env.resend_verification_threshold)
+ r = participant.add_email(address, resend_threshold=website.env.resend_verification_threshold)
if r:
msg = _("A verification email has been sent to {0}.", address)
else: