diff --git a/gratipay/models/package/__init__.py b/gratipay/models/package/__init__.py
index 24af56d724..52b3464c0c 100644
--- a/gratipay/models/package/__init__.py
+++ b/gratipay/models/package/__init__.py
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals
+import uuid
+
+from gratipay.models.team import Team
from postgres.orm import Model
@@ -10,6 +13,14 @@
class Package(Model):
"""Represent a gratipackage. :-)
+
+ Packages are entities on open source package managers; `npm
+ `_ is the only one we support so far. Each package
+ on npm has a page on Gratipay with an URL of the form ``/on/npm/foo/``.
+ Packages can be claimed by Gratipay participants, at which point we create
+ a :py:class:`~gratipay.models.team.Team` for them under the hood so they
+ can start accepting payments.
+
"""
typname = 'packages'
@@ -40,3 +51,38 @@ 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))
+
+
+ @property
+ def team(self):
+ """A computed attribute, the :py:class:`~gratipay.models.team.Team`
+ linked to this package if there is one, otherwise ``None``. Makes a
+ database call.
+ """
+ return self._load_team(self.db)
+
+
+ def ensure_team(self, cursor, owner):
+ """Given a db cursor and :py:class:`Participant`, insert into ``teams`` if need be.
+ """
+ team = self._load_team(cursor)
+ if not team:
+ slug = str(uuid.uuid4()).lower()
+ team = Team.insert( slug=slug
+ , slug_lower=slug
+ , name=slug
+ , homepage=''
+ , product_or_service=''
+ , owner=owner
+ , _cursor=cursor
+ )
+ cursor.run('INSERT INTO teams_to_packages (team_id, package_id) '
+ 'VALUES (%s, %s)', (team.id, self.id))
+ return team
+
+
+ def _load_team(self, cursor):
+ return cursor.one( 'SELECT t.*::teams FROM teams t WHERE t.id='
+ '(SELECT team_id FROM teams_to_packages tp WHERE tp.package_id=%s)'
+ , (self.id,)
+ )
diff --git a/gratipay/models/participant/email.py b/gratipay/models/participant/email.py
index fb8fafda3b..73ee8d317b 100644
--- a/gratipay/models/participant/email.py
+++ b/gratipay/models/participant/email.py
@@ -48,7 +48,8 @@ def start_email_verification(self, email, *packages):
verification email for an unverified email address.
:param unicode email: the email address to add
- :param Package packages: packages to optionally also verify ownership of
+ :param gratipay.models.package.Package packages: packages to optionally
+ also verify ownership of
:returns: ``None``
@@ -201,63 +202,111 @@ def start_package_claims(self, c, nonce, *packages):
)
- def update_email(self, email):
- """Set the email address for the participant.
+ def set_primary_email(self, email, cursor=None):
+ """Set the primary email address for the participant.
"""
- if not getattr(self.get_email(email), 'verified', False):
- raise EmailNotVerified()
- username = self.username
- with self.db.get_cursor() as c:
- self.app.add_event( c
- , 'participant'
- , dict(id=self.id, action='set', values=dict(primary_email=email))
- )
- c.run("""
- UPDATE participants
- SET email_address=%(email)s
- WHERE username=%(username)s
- """, locals())
+ if cursor:
+ self._set_primary_email(email, cursor)
+ else:
+ with self.db.get_cursor() as cursor:
+ self._set_primary_email(email, cursor)
self.set_attributes(email_address=email)
- def verify_email(self, email, nonce):
- if '' in (email, nonce):
+ def _set_primary_email(self, email, cursor):
+ if not getattr(self.get_email(email, cursor), 'verified', False):
+ raise EmailNotVerified()
+ self.app.add_event( cursor
+ , 'participant'
+ , dict(id=self.id, action='set', values=dict(primary_email=email))
+ )
+ cursor.run("""
+ UPDATE participants
+ SET email_address=%(email)s
+ WHERE username=%(username)s
+ """, dict(email=email, username=self.username))
+
+
+ def finish_email_verification(self, email, nonce):
+ if '' in (email.strip(), nonce.strip()):
return VERIFICATION_MISSING
- r = self.get_email(email)
- if r is None:
- return VERIFICATION_FAILED
- if r.verified:
- assert r.nonce is None # and therefore, order of conditions matters
- return VERIFICATION_REDUNDANT
- if not constant_time_compare(r.nonce, nonce):
- return VERIFICATION_FAILED
- if (utcnow() - r.verification_start) > EMAIL_HASH_TIMEOUT:
- return VERIFICATION_EXPIRED
- try:
- self.db.run("""
- UPDATE emails
- SET verified=true, verification_end=now(), nonce=NULL
- WHERE participant_id=%s
- AND address=%s
- AND verified IS NULL
- """, (self.id, email))
- except IntegrityError:
- return VERIFICATION_STYMIED
+ with self.db.get_cursor() as cursor:
+ record = self.get_email(email, cursor, and_lock=True)
+ if record is None:
+ return VERIFICATION_FAILED
+ packages = self.get_packages_claiming(cursor, nonce)
+ if record.verified and not packages:
+ assert record.nonce is None # and therefore, order of conditions matters
+ return VERIFICATION_REDUNDANT
+ if not constant_time_compare(record.nonce, nonce):
+ return VERIFICATION_FAILED
+ if (utcnow() - record.verification_start) > EMAIL_HASH_TIMEOUT:
+ return VERIFICATION_EXPIRED
+ try:
+ self.finish_package_claims(cursor, nonce, *packages)
+ self.save_email_address(cursor, email)
+ except IntegrityError:
+ return VERIFICATION_STYMIED
+ return VERIFICATION_SUCCEEDED
+
+
+ def get_packages_claiming(self, cursor, nonce):
+ """Given a nonce, return :py:class:`~gratipay.models.package.Package`
+ objects associated with it.
+ """
+ return cursor.all("""
+ SELECT p.*::packages
+ FROM packages p
+ JOIN claims c
+ ON p.id = c.package_id
+ WHERE c.nonce=%s
+ """, (nonce,))
+
+ def save_email_address(self, cursor, address):
+ """Given an email address, modify the database.
+ """
+ cursor.run("""
+ UPDATE emails
+ SET verified=true, verification_end=now(), nonce=NULL
+ WHERE participant_id=%s
+ AND address=%s
+ AND verified IS NULL
+ """, (self.id, address))
if not self.email_address:
- self.update_email(email)
- return VERIFICATION_SUCCEEDED
+ self.set_primary_email(address, cursor)
+
+
+ def finish_package_claims(self, cursor, nonce, *packages):
+ """Create teams if needed and associate them with the packages.
+ """
+ cursor.run('DELETE FROM claims WHERE nonce=%s', (nonce,))
+ package_ids = []
+ for package in packages:
+ package.ensure_team(cursor, self)
+ package_ids.append(package.id)
+ self.app.add_event( cursor
+ , 'participant'
+ , dict( id=self.id
+ , action='finish-claim'
+ , values=dict(package_ids=package_ids)
+ )
+ )
- def get_email(self, email):
+ def get_email(self, address, cursor=None, and_lock=False):
"""Return a record for a single email address on file for this participant.
+
+ :param unicode address: the email address for which to get a record
+ :param Cursor cursor: a database cursor; if ``None``, we'll use ``self.db``
+ :param and_lock: if True, we will acquire a write-lock on the email record before returning
+ :returns: a database record (a named tuple)
+
"""
- return self.db.one("""
- SELECT *
- FROM emails
- WHERE participant_id=%s
- AND address=%s
- """, (self.id, email))
+ sql = 'SELECT * FROM emails WHERE participant_id=%s AND address=%s'
+ if and_lock:
+ sql += ' FOR UPDATE'
+ return (cursor or self.db).one(sql, (self.id, address))
def get_emails(self):
diff --git a/gratipay/models/team/__init__.py b/gratipay/models/team/__init__.py
index 4f371a3d1e..0e602bfac6 100644
--- a/gratipay/models/team/__init__.py
+++ b/gratipay/models/team/__init__.py
@@ -13,6 +13,7 @@
from .available import Available
from .closing import Closing
from .membership import Membership
+from .package import Package
from .takes import Takes
from .tip_migration import TipMigration
@@ -36,7 +37,7 @@ def slugize(name):
return slug
-class Team(Model, Available, Closing, Membership, Takes, TipMigration):
+class Team(Model, Available, Closing, Membership, Package, Takes, TipMigration):
"""Represent a Gratipay team.
"""
@@ -98,9 +99,10 @@ def _from_thing(cls, thing, value):
@classmethod
def insert(cls, owner, **fields):
+ cursor = fields.pop('_cursor') if '_cursor' in fields else None
fields['slug_lower'] = fields['slug'].lower()
fields['owner'] = owner.username
- return cls.db.one("""
+ return (cursor or cls.db).one("""
INSERT INTO teams
(slug, slug_lower, name, homepage,
diff --git a/gratipay/models/team/package.py b/gratipay/models/team/package.py
new file mode 100644
index 0000000000..50de9a622a
--- /dev/null
+++ b/gratipay/models/team/package.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+
+class Package(object):
+ """A :py:class:`~gratipay.models.team.Team` can be associated with :py:class:`Package`.
+ """
+
+ @property
+ def package(self):
+ """A computed attribute, the
+ :py:class:`~gratipay.models.package.Package` linked to this team if
+ there is one, otherwise ``None``. Makes a database call.
+ """
+ return self._load_package(self.db)
+
+
+ def _load_package(self, cursor):
+ return cursor.one( 'SELECT p.*::packages FROM packages p WHERE p.id='
+ '(SELECT package_id FROM teams_to_packages tp WHERE tp.team_id=%s)'
+ , (self.id,)
+ )
diff --git a/sql/branch.sql b/sql/branch.sql
index 8d39aa1369..4052aa0ec0 100644
--- a/sql/branch.sql
+++ b/sql/branch.sql
@@ -9,4 +9,9 @@ BEGIN;
, UNIQUE(nonce, package_id)
);
+ CREATE TABLE teams_to_packages
+ ( team_id bigint UNIQUE REFERENCES teams(id) ON DELETE RESTRICT
+ , package_id bigint UNIQUE REFERENCES packages(id) ON DELETE RESTRICT
+ );
+
END;
diff --git a/tests/py/test_email.py b/tests/py/test_email.py
index 511a7c3202..080d36ffd1 100644
--- a/tests/py/test_email.py
+++ b/tests/py/test_email.py
@@ -1,9 +1,11 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import json
+import Queue
import sys
-
+import threading
import urllib
+
from pytest import raises
from gratipay.exceptions import CannotRemovePrimaryEmail, EmailTaken, EmailNotVerified
@@ -11,6 +13,7 @@
from gratipay.exceptions import EmailNotOnFile, ProblemChangingEmail
from gratipay.testing import P, Harness
from gratipay.testing.email import QueuedEmailHarness, SentEmailHarness
+from gratipay.models.package import Package
from gratipay.models.participant import email as _email
from gratipay.utils import encode_for_querystring
from gratipay.cli import queue_branch_email as _queue_branch_email
@@ -25,7 +28,7 @@ def setUp(self):
def add(self, participant, address, _flush=False):
participant.start_email_verification(address)
nonce = participant.get_email(address).nonce
- r = participant.verify_email(address, nonce)
+ r = participant.finish_email_verification(address, nonce)
assert r == _email.VERIFICATION_SUCCEEDED
if _flush:
self.app.email_queue.flush()
@@ -57,7 +60,7 @@ def hit_email_spt(self, action, address, user='alice', package_ids=[], should_fa
response.render_body({'_': lambda a: a})
return response
- def verify_email(self, email, nonce, username='alice', should_fail=False):
+ def finish_email_verification(self, email, nonce, username='alice', should_fail=False):
# Email address is encoded in url.
url = '/~%s/emails/verify.html?email2=%s&nonce=%s'
url %= (username, encode_for_querystring(email), nonce)
@@ -67,7 +70,7 @@ def verify_email(self, email, nonce, username='alice', should_fail=False):
def verify_and_change_email(self, old_email, new_email, username='alice', _flush=True):
self.hit_email_spt('add-email', old_email)
nonce = P(username).get_email(old_email).nonce
- self.verify_email(old_email, nonce)
+ self.finish_email_verification(old_email, nonce)
self.hit_email_spt('add-email', new_email)
if _flush:
self.app.email_queue.flush()
@@ -134,15 +137,15 @@ def test_post_too_quickly_is_400(self):
assert 'too quickly' in response.body
def test_verify_email_without_adding_email(self):
- response = self.verify_email('', 'sample-nonce')
+ response = self.finish_email_verification('', 'sample-nonce')
assert 'Bad Info' in response.body
def test_verify_email_wrong_nonce(self):
self.hit_email_spt('add-email', 'alice@example.com')
nonce = 'fake-nonce'
- r = self.alice.verify_email('alice@gratipay.com', nonce)
+ r = self.alice.finish_email_verification('alice@gratipay.com', nonce)
assert r == _email.VERIFICATION_FAILED
- self.verify_email('alice@example.com', nonce)
+ self.finish_email_verification('alice@example.com', nonce)
expected = None
actual = P('alice').email_address
assert expected == actual
@@ -151,8 +154,8 @@ def test_verify_email_a_second_time_returns_redundant(self):
address = 'alice@example.com'
self.hit_email_spt('add-email', address)
nonce = self.alice.get_email(address).nonce
- r = self.alice.verify_email(address, nonce)
- r = self.alice.verify_email(address, nonce)
+ r = self.alice.finish_email_verification(address, nonce)
+ r = self.alice.finish_email_verification(address, nonce)
assert r == _email.VERIFICATION_REDUNDANT
def test_verify_email_expired_nonce(self):
@@ -164,18 +167,26 @@ def test_verify_email_expired_nonce(self):
WHERE participant_id = %s;
""", (self.alice.id,))
nonce = self.alice.get_email(address).nonce
- r = self.alice.verify_email(address, nonce)
+ r = self.alice.finish_email_verification(address, nonce)
assert r == _email.VERIFICATION_EXPIRED
actual = P('alice').email_address
assert actual == None
- def test_verify_email(self):
+ def test_finish_email_verification(self):
self.hit_email_spt('add-email', 'alice@example.com')
nonce = self.alice.get_email('alice@example.com').nonce
- self.verify_email('alice@example.com', nonce)
- expected = 'alice@example.com'
- actual = P('alice').email_address
- assert expected == actual
+ assert self.finish_email_verification('alice@example.com', nonce).code == 200
+ assert P('alice').email_address == 'alice@example.com'
+
+ def test_empty_email_results_in_missing(self):
+ for empty in ('', ' '):
+ result = self.alice.finish_email_verification(empty, 'foobar')
+ assert result == _email.VERIFICATION_MISSING
+
+ def test_empty_nonce_results_in_missing(self):
+ for empty in ('', ' '):
+ result = self.alice.finish_email_verification('foobar', empty)
+ assert result == _email.VERIFICATION_MISSING
def test_email_verification_is_backwards_compatible(self):
"""Test email verification still works with unencoded email in verification link.
@@ -202,7 +213,7 @@ def test_get_emails(self):
def test_verify_email_after_update(self):
self.verify_and_change_email('alice@example.com', 'alice@example.net')
nonce = self.alice.get_email('alice@example.net').nonce
- self.verify_email('alice@example.net', nonce)
+ self.finish_email_verification('alice@example.net', nonce)
expected = 'alice@example.com'
actual = P('alice').email_address
assert expected == actual
@@ -301,7 +312,7 @@ def test_cannot_update_email_to_already_verified(self):
with self.assertRaises(EmailTaken):
bob.start_email_verification('alice@gratipay.com')
nonce = bob.get_email('alice@gratipay.com').nonce
- bob.verify_email('alice@gratipay.com', nonce)
+ bob.finish_email_verification('alice@gratipay.com', nonce)
email_alice = P('alice').email_address
assert email_alice == 'alice@gratipay.com'
@@ -597,7 +608,7 @@ def check(self, *package_names, **kw):
def preverify(self, address='alice@example.com'):
self.alice.start_email_verification(address)
nonce = self.alice.get_email(address).nonce
- self.alice.verify_email(address, nonce)
+ self.alice.finish_email_verification(address, nonce)
class VerificationMessage(VerificationBase):
@@ -682,3 +693,117 @@ def test_sends_notice_for_unverified_address_and_multiple_packages(self):
html, text = self.check('foo', 'bar')
assert ' connecting alice@example.com and 2 npm packages ' in html
assert ' connecting alice@example.com and 2 npm packages ' in text
+
+
+class PackageLinking(VerificationBase):
+
+ address = 'alice@example.com'
+
+ def start(self, address, *package_names):
+ packages = [self.make_package(name=name, emails=[address]) for name in package_names]
+ self.alice.start_email_verification(address, *packages)
+ return self.alice.get_email(address).nonce
+
+ def check(self, *package_names):
+ nonce = self.start(self.address, *package_names)
+ retval = self.alice.finish_email_verification(self.address, nonce)
+ assert retval == _email.VERIFICATION_SUCCEEDED
+ assert self.alice.email_address == P('alice').email_address == self.address
+ for name in package_names:
+ package = Package.from_names('npm', name)
+ assert package.team.package == package
+
+
+ def test_preverify_preverifies(self):
+ assert self.alice.email_address is None
+ self.preverify()
+ assert self.alice.email_address == self.address
+
+
+ def test_unverified_address_and_no_packages_succeeds(self):
+ self.check()
+
+ def test_unverified_address_and_one_package_succeeds(self):
+ self.check('foo')
+
+ def test_unverified_address_and_multiple_packages_succeeds(self):
+ self.check('foo', 'bar')
+
+ def test_verified_address_and_no_packages_is_a_no_go(self):
+ self.preverify()
+ raises(EmailAlreadyVerified, self.check)
+
+ def test_verified_address_and_one_package_succeeds(self):
+ self.preverify()
+ self.check('foo')
+
+ def test_verified_address_and_multiple_packages_succeeds(self):
+ self.preverify()
+ self.check('foo', 'bar')
+
+
+ def test_bob_cannot_steal_a_package_claim_from_alice(self):
+ foo = self.make_package()
+ self.alice.start_email_verification(self.address, foo)
+ nonce = self.alice.get_email(self.address).nonce
+
+ # u so bad bob!
+ bob = self.make_participant('bob', claimed_time='now')
+ bob.start_email_verification(self.address, foo)
+ result = bob.finish_email_verification(self.address, nonce) # using alice's nonce, even!
+ assert result == _email.VERIFICATION_FAILED
+ assert len(bob.get_teams()) == 0
+
+ result = self.alice.finish_email_verification(self.address, nonce)
+ assert result == _email.VERIFICATION_SUCCEEDED
+ teams = self.alice.get_teams()
+ assert len(teams) == 1
+ assert teams[0].package == foo
+
+
+ def test_while_we_are_at_it_that_packages_have_unique_teams_that_survive_comparison(self):
+ self.test_verified_address_and_multiple_packages_succeeds()
+
+ foo = Package.from_names('npm', 'foo')
+ bar = Package.from_names('npm', 'bar')
+
+ assert foo.team == foo.team
+ assert bar.team == bar.team
+ assert foo.team != bar.team
+
+
+ def test_finishing_email_verification_is_thread_safe(self):
+ foo = self.make_package()
+ self.alice.start_email_verification(self.address, foo)
+ nonce = self.alice.get_email(self.address).nonce
+
+ results = {}
+ def finish():
+ key = threading.current_thread().ident
+ results[key] = self.alice.finish_email_verification(self.address, nonce)
+
+ def t():
+ t = threading.Thread(target=finish)
+ t.daemon = True
+ return t
+
+ go = Queue.Queue()
+ def monkey(self, *a, **kw):
+ old_ensure_team(self, *a, **kw)
+ go.get()
+ old_ensure_team = Package.ensure_team
+ Package.ensure_team = monkey
+
+ try:
+ a, b = t(), t()
+ a.start()
+ b.start()
+ go.put('')
+ go.put('')
+ b.join()
+ a.join()
+ finally:
+ Package.ensure_team = old_ensure_team
+
+ assert results[a.ident] == _email.VERIFICATION_SUCCEEDED
+ assert results[b.ident] == _email.VERIFICATION_REDUNDANT
diff --git a/tests/py/test_packages.py b/tests/py/test_packages.py
index b97e888377..1d48d5e5d9 100644
--- a/tests/py/test_packages.py
+++ b/tests/py/test_packages.py
@@ -3,6 +3,8 @@
from gratipay.models.package import NPM, Package
from gratipay.testing import Harness
+from psycopg2 import IntegrityError
+from pytest import raises
class TestPackage(Harness):
@@ -14,3 +16,50 @@ 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 Linking(Harness):
+
+ def test_package_team_is_none(self):
+ foo = self.make_package()
+ assert foo.team is None
+
+ def test_team_package_is_none(self):
+ foo = self.make_team()
+ assert foo.package is None
+
+ def test_can_link_to_a_new_team(self):
+ alice = self.make_participant('alice')
+ foo = self.make_package()
+ with self.db.get_cursor() as c:
+ foo.ensure_team(c, alice)
+ team = foo._load_team(c)
+ assert team.package == foo
+ assert foo.team == team
+ return alice, foo, team
+
+ def test_linking_is_idempotent(self):
+ alice, package, _team = self.test_can_link_to_a_new_team()
+ for i in range(5):
+ with self.db.get_cursor() as c:
+ package.ensure_team(c, alice)
+ team = package._load_team(c)
+ assert team == _team
+
+ def test_team_can_only_be_linked_from_one_package(self):
+ alice, package, team = self.test_can_link_to_a_new_team()
+ bar = self.make_package(name='bar')
+ raises( IntegrityError
+ , self.db.run
+ , 'INSERT INTO teams_to_packages (team_id, package_id) VALUES (%s, %s)'
+ , (team.id, bar.id)
+ )
+
+ def test_package_can_only_be_linked_from_one_team(self):
+ alice, package, team = self.test_can_link_to_a_new_team()
+ bar = self.make_team(name='Bar')
+ raises( IntegrityError
+ , self.db.run
+ , 'INSERT INTO teams_to_packages (team_id, package_id) VALUES (%s, %s)'
+ , (bar.id, package.id)
+ )
diff --git a/tests/py/test_take_over.py b/tests/py/test_take_over.py
index 31e8f13c4e..de35b707e4 100644
--- a/tests/py/test_take_over.py
+++ b/tests/py/test_take_over.py
@@ -153,7 +153,8 @@ def test_take_over_is_fine_with_identity_info_on_primary(self):
TT = self.db.one("SELECT id FROM countries WHERE code='TT'")
alice = self.make_participant('alice')
alice.start_email_verification('alice@example.com')
- alice.verify_email('alice@example.com', alice.get_email('alice@example.com').nonce)
+ nonce = alice.get_email('alice@example.com').nonce
+ alice.finish_email_verification('alice@example.com', nonce)
alice.store_identity_info(TT, 'nothing-enforced', {})
bob_github = self.make_elsewhere('github', 2, 'bob')
@@ -169,7 +170,7 @@ def test_take_over_fails_if_secondary_has_identity_info(self):
bob_github = self.make_elsewhere('github', 2, 'bob')
bob = bob_github.opt_in('bob')[0].participant
bob.start_email_verification('bob@example.com')
- bob.verify_email('bob@example.com', bob.get_email('bob@example.com').nonce)
+ bob.finish_email_verification('bob@example.com', bob.get_email('bob@example.com').nonce)
bob.store_identity_info(TT, 'nothing-enforced', {})
pytest.raises(WontTakeOverWithIdentities, alice.take_over, bob_github)
@@ -187,11 +188,13 @@ def test_email_addresses_merging(self):
alice.start_email_verification('alice@example.com')
alice.start_email_verification('alice@example.net')
alice.start_email_verification('alice@example.org')
- alice.verify_email('alice@example.org', alice.get_email('alice@example.org').nonce)
+ nonce = alice.get_email('alice@example.org').nonce
+ alice.finish_email_verification('alice@example.org', nonce)
bob_github = self.make_elsewhere('github', 2, 'bob')
bob = bob_github.opt_in('bob')[0].participant
bob.start_email_verification('alice@example.com')
- bob.verify_email('alice@example.com', bob.get_email('alice@example.com').nonce)
+ nonce = bob.get_email('alice@example.com').nonce
+ bob.finish_email_verification('alice@example.com', nonce)
bob.start_email_verification('alice@example.net')
bob.start_email_verification('bob@example.net')
alice.take_over(bob_github, have_confirmation=True)
diff --git a/www/~/%username/emails/modify.json.spt b/www/~/%username/emails/modify.json.spt
index e7d7452dc4..9b75a54e4d 100644
--- a/www/~/%username/emails/modify.json.spt
+++ b/www/~/%username/emails/modify.json.spt
@@ -46,7 +46,7 @@ if action in ('add-email', 'resend', 'start-verification'):
participant.start_email_verification(address, *packages)
msg = _("Check your inbox for a verification link.")
elif action == 'set-primary':
- participant.update_email(address)
+ participant.set_primary_email(address)
elif action == 'remove':
participant.remove_email(address)
else:
diff --git a/www/~/%username/emails/verify.html.spt b/www/~/%username/emails/verify.html.spt
index d607ed5a72..c93f8f01f6 100644
--- a/www/~/%username/emails/verify.html.spt
+++ b/www/~/%username/emails/verify.html.spt
@@ -22,7 +22,7 @@ if participant == user.participant:
else:
email_address = decode_from_querystring(request.qs.get('email2', ''), default='')
nonce = request.qs.get('nonce', '')
- result = participant.verify_email(email_address, nonce)
+ result = participant.finish_email_verification(email_address, nonce)
if not participant.email_lang:
participant.set_email_lang(request.headers.get("Accept-Language"))