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: