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

implement has_verified_identity #4003

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions gratipay/models/participant/mixins/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ class IdentityMixin(object):

"""

#: ``True`` if the participant has at least one verified identity on file,
#: ``False`` otherwise. This attribute is read-only. It is updated with
#: :py:meth:`set_identity_verification` and :py:meth:`clear_identity`.

has_verified_identity = False

def store_identity_info(self, country_id, schema_name, info):
"""Store the participant's national identity information for a given country.

Expand Down Expand Up @@ -219,6 +225,7 @@ def set_identity_verification(self, country_id, is_verified):
)

add_event(cursor, 'participant', payload)
self._update_has_verified_identity(cursor)


def clear_identity(self, country_id):
Expand All @@ -243,6 +250,25 @@ def clear_identity(self, country_id):
, action='clear identity'
)
add_event(cursor, 'participant', payload)
self._update_has_verified_identity(cursor)


def _update_has_verified_identity(self, cursor):
has_verified_identity = cursor.one("""

WITH verified_identities AS
( SELECT *
FROM participant_identities
WHERE participant_id=%(participant_id)s
AND is_verified
)
UPDATE participants
SET has_verified_identity=(SELECT count(*) FROM verified_identities) > 0
WHERE id=%(participant_id)s
RETURNING has_verified_identity

""", dict(participant_id=self.id))
self.set_attributes(has_verified_identity=has_verified_identity)


# Rekeying
Expand Down
3 changes: 3 additions & 0 deletions sql/branch.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- participants.has_verified_identity

ALTER TABLE participants ADD COLUMN has_verified_identity bool NOT NULL DEFAULT false;
93 changes: 93 additions & 0 deletions tests/py/test_participant_identities.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

from cryptography.fernet import InvalidToken
from gratipay.testing import Harness
from gratipay.models.participant import Participant
from gratipay.models.participant.mixins import identity, Identity
from gratipay.models.participant.mixins.identity import _validate_info, rekey
from gratipay.models.participant.mixins.identity import ParticipantIdentityInfoInvalid
from gratipay.models.participant.mixins.identity import ParticipantIdentitySchemaUnknown
from gratipay.security.crypto import EncryptingPacker, Fernet
from postgres.orm import ReadOnly
from psycopg2 import IntegrityError
from pytest import raises

Expand Down Expand Up @@ -250,6 +252,97 @@ def test_ci_still_logs_an_event_when_noop(self):
self.assert_events(self.crusher.id, [None], [self.TT], ['clear identity'])


# hvi - has_verified_identity

def test_hvi_defaults_to_false(self):
assert self.crusher.has_verified_identity is False

def test_hvi_is_read_only(self):
with raises(ReadOnly):
self.crusher.has_verified_identity = True

def test_hvi_becomes_true_when_an_identity_is_verified(self):
self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {})
self.crusher.set_identity_verification(self.TTO, True)
assert self.crusher.has_verified_identity
assert Participant.from_username('crusher').has_verified_identity

def test_hvi_becomes_false_when_the_identity_is_unverified(self):
self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {})
self.crusher.set_identity_verification(self.TTO, True)
self.crusher.set_identity_verification(self.TTO, False)
assert not self.crusher.has_verified_identity
assert not Participant.from_username('crusher').has_verified_identity

def test_hvi_stays_true_when_a_secondary_identity_is_verified(self):
self.crusher.store_identity_info(self.USA, 'nothing-enforced', {})
self.crusher.set_identity_verification(self.USA, True)
self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {})
self.crusher.set_identity_verification(self.TTO, True)
assert self.crusher.has_verified_identity
assert Participant.from_username('crusher').has_verified_identity

def test_hvi_stays_true_when_the_secondary_identity_is_unverified(self):
self.crusher.store_identity_info(self.USA, 'nothing-enforced', {})
self.crusher.set_identity_verification(self.USA, True)
self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {})
self.crusher.set_identity_verification(self.TTO, True)
self.crusher.set_identity_verification(self.TTO, False)
assert self.crusher.has_verified_identity
assert Participant.from_username('crusher').has_verified_identity

def test_hvi_goes_back_to_false_when_both_are_unverified(self):
self.crusher.store_identity_info(self.USA, 'nothing-enforced', {})
self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {})
self.crusher.set_identity_verification(self.TTO, True)
self.crusher.set_identity_verification(self.USA, True)
self.crusher.set_identity_verification(self.TTO, False)
self.crusher.set_identity_verification(self.USA, False)
assert not self.crusher.has_verified_identity
assert not Participant.from_username('crusher').has_verified_identity

def test_hvi_changes_are_scoped_to_a_participant(self):
self.crusher.store_identity_info(self.USA, 'nothing-enforced', {})

bruiser = self.make_participant('bruiser', email_address='[email protected]')
bruiser.store_identity_info(self.USA, 'nothing-enforced', {})

self.crusher.set_identity_verification(self.USA, True)

assert self.crusher.has_verified_identity
assert Participant.from_username('crusher').has_verified_identity
assert not bruiser.has_verified_identity
assert not Participant.from_username('bruiser').has_verified_identity

def test_hvi_resets_when_identity_is_cleared(self):
self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {})
self.crusher.set_identity_verification(self.TTO, True)
self.crusher.clear_identity(self.TTO)
assert not self.crusher.has_verified_identity
assert not Participant.from_username('crusher').has_verified_identity

def test_hvi_doesnt_reset_when_penultimate_identity_is_cleared(self):
self.crusher.store_identity_info(self.USA, 'nothing-enforced', {})
self.crusher.set_identity_verification(self.USA, True)
self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {})
self.crusher.set_identity_verification(self.TTO, True)
self.crusher.set_identity_verification(self.TTO, False)
self.crusher.clear_identity(self.TTO)
assert self.crusher.has_verified_identity
assert Participant.from_username('crusher').has_verified_identity

def test_hvi_does_reset_when_both_identities_are_cleared(self):
self.crusher.store_identity_info(self.USA, 'nothing-enforced', {})
self.crusher.store_identity_info(self.TTO, 'nothing-enforced', {})
self.crusher.set_identity_verification(self.USA, True)
self.crusher.set_identity_verification(self.TTO, True)
self.crusher.set_identity_verification(self.TTO, False)
self.crusher.set_identity_verification(self.USA, False)
self.crusher.clear_identity(self.TTO)
assert not self.crusher.has_verified_identity
assert not Participant.from_username('crusher').has_verified_identity


# fine - fail_if_no_email

def test_fine_fails_if_no_email(self):
Expand Down
2 changes: 1 addition & 1 deletion www/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1959
1960