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

Commit

Permalink
Merge pull request #4034 from gratipay/identity-form
Browse files Browse the repository at this point in the history
Identity form
  • Loading branch information
chadwhitacre committed May 12, 2016
2 parents 5ad2243 + 5d8c512 commit da4cace
Show file tree
Hide file tree
Showing 14 changed files with 533 additions and 154 deletions.
14 changes: 14 additions & 0 deletions emails/identity-viewed.spt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{{ _("Identity Viewed") }}
[---] text/html
{{ _( "This is a transactional email to let you know that {a_viewer}{viewer}{_a} viewed your identity information for {a_country}{country_name}{_a} on Gratipay."
, viewer=viewer
, country_name=country_name
, a_viewer=('<a href="https://gratipay.com/~{}/">'|safe).format(viewer)
, a_country=('<a href="https://gratipay.com/about/me/identities/{}">'|safe).format(country_code)
, _a='</a>'|safe
) }}
[---] text/plain
{{ _( "This is a transactional email to let you know that {viewer} viewed your identity information for {country_name} on Gratipay."
, viewer=viewer
, country_name=country_name
) }}
4 changes: 4 additions & 0 deletions gratipay/models/country.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ class Country(Model):
"""
typname = 'countries'

@classmethod
def from_code(cls, code):
return cls.db.one("SELECT countries.*::countries FROM countries WHERE code=%s", (code,))
1 change: 1 addition & 0 deletions js/gratipay.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Gratipay.init = function() {
Gratipay.signOut();
Gratipay.payments.initSupportGratipay();
Gratipay.tabs.init();
Gratipay.countryChooser.init();
};

Gratipay.warnOffUsersFromDeveloperConsole = function() {
Expand Down
18 changes: 18 additions & 0 deletions js/gratipay/countryChooser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Gratipay.countryChooser = {}

Gratipay.countryChooser.init = function() {
$('.open-country-chooser').click(Gratipay.countryChooser.open);
$('.close-country-chooser').click(Gratipay.countryChooser.close);
$('#grayout').click(Gratipay.countryChooser.close);
};

Gratipay.countryChooser.open = function() {
$('.open-country-chooser').blur();
$('#grayout').show()
$('#country-chooser').show();
};

Gratipay.countryChooser.close = function() {
$('#country-chooser').hide()
$('#grayout').hide()
};
17 changes: 16 additions & 1 deletion scss/pages/cc-ba.scss → scss/components/long-form.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.cc-ba {
.long-form {
width: 300px;

form {
Expand Down Expand Up @@ -113,4 +113,19 @@
input.invalid:focus + .invalid-msg {
display: block;
}

.danger-zone {
margin-top: 64px;
border: 1px solid $red;
@include border-radius(5px);
padding: 20px;
h2 {
margin: 0 0 10px;
padding: 0;
color: $red;
}
button {
background: $red;
}
}
}
139 changes: 139 additions & 0 deletions scss/pages/identities.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#identities {
padding-top: 20px;

.card {
height: 96px;
width: 46%;
float: left;
margin: 0 8% 8% 0;
padding: 20px;
position: relative;
@include border-radius(5px);
@include box-shadow(0, 0, 10px, $black);

&:nth-child(even) {
margin-right: 0;
}

&.verified {
background-image: radial-gradient( circle at 36px 60px
, lighten($green, 55%) 0%
, lighten($green, 30%) 100%
);
color: $green;
}

&.unverified {
color: $red;
}

&.add {
background: $lightest-gray;
color: $medium-gray;
border-style: dashed;
@include box-shadow(0,0,0);
border: 2px dashed $darker-gray;

&:hover {
color: $black;
background-image: radial-gradient( ellipse farthest-corner at 50% 50%
, $white 0%
, $lightest-gray 50%
);
}
}

img {
position: absolute;
bottom: 20px;
left: 20px;
}

h2 {
white-space: nowrap;
margin: 0;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
}

.status {
position: absolute;
bottom: 20px;
right: 20px;
font: normal 9px $Mono;
}
}

#country-chooser {
z-index: 1001;
display: none;
background: white;
@include border-radius(5px);
@include box-shadow(0, 0, 10px, $black);
position: fixed;
width: 240px;
height: 50vh;
top: 25%;
left: 50%;
margin-left: -120px;

header {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 240px;
height: 36px;
background: white;
border-bottom: 1px solid $black;
@include border-radius(5px 5px 0 0);
@include box-shadow(0, 0, 10px, $black);

h2 {
margin: 0;
padding: 12px 20px 0;
}
button {
position: absolute;
top: 5px;
right: 20px;
}
}

section {
overflow: auto;
height: 50vh;
z-index: 0;
margin-top: 36px;
background: $lightest-gray;
@include border-radius(0 0 5px 5px);
@include box-shadow(0, 0, 10px, $black);

a {
padding: 5px 20px;
display: block;
img {
margin: 0 5px -10px 0;
}
span {
font: bold 18px/18px $Ideal;
}
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}

#grayout {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
background: transparentize($black, 0.3);
z-index: 1000;
display: none;
}
}
1 change: 1 addition & 0 deletions scss/variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ $medium-gray: #999;
$gray: #555;
$light-gray: #DDD;
$lighter-gray: #EEE;
$lightest-gray: #F6F6F6;

$red: #C00;
$light-red: #F99;
Expand Down
135 changes: 135 additions & 0 deletions tests/py/test_identity_pages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
from __future__ import absolute_import, division, print_function, unicode_literals

from gratipay.models.country import Country
from gratipay.models.participant import Participant
from gratipay.testing.emails import EmailHarness


class Tests(EmailHarness):

def setUp(self):
super(Tests, self).setUp()
self.make_participant('alice', claimed_time='now', is_admin=True)
self.make_participant('whit537', id=1451, email_address='[email protected]',
claimed_time='now', is_admin=True)
self.make_participant('bob', claimed_time='now', email_address='[email protected]')
self.verify('bob', 'TT')

def identify(self, username, *codes):
participant = Participant.from_username(username)
for code in codes:
country_id = Country.from_code(code).id
participant.store_identity_info(country_id, 'nothing-enforced', {})
return participant

def verify(self, username, *codes):
participant = Participant.from_username(username)
for code in codes:
country_id = Country.from_code(code).id
participant.store_identity_info(country_id, 'nothing-enforced', {})
participant.set_identity_verification(country_id, True)
return participant


# il - identities listing

def test_il_is_403_for_anon(self):
assert self.client.GxT('/~bob/identities/').code == 403

def test_il_is_403_for_non_admin(self):
assert self.client.GxT('/~bob/identities/').code == 403

def test_il_is_200_for_self(self):
assert self.client.GET('/~bob/identities/', auth_as='bob').code == 200

def test_il_is_200_for_admin(self):
assert self.client.GET('/~bob/identities/', auth_as='alice').code == 200


# ip - identity page

def test_ip_disallows_methods(self):
assert self.client.hxt('HEAD', '/~bob/identities/TT').code == 405

def test_ip_is_403_for_anon(self):
assert self.client.GxT('/~bob/identities/TT').code == 403

def test_ip_is_403_for_non_admin(self):
assert self.client.GxT('/~bob/identities/TT').code == 403

def test_ip_is_200_for_self(self):
assert self.client.GET('/~bob/identities/TT', auth_as='bob').code == 200

def test_ip_is_403_for_most_admins(self):
assert self.client.GxT('/~bob/identities/TT', auth_as='alice').code == 403

def test_ip_is_200_for_whit537_yikes_O_O(self):
assert self.client.GET('/~bob/identities/TT', auth_as='whit537').code == 200

def test_ip_notifies_participant_when_whit537_views(self):
self.client.GET('/~bob/identities/TT', auth_as='whit537')
assert 'whit537 viewed your identity' in self.get_last_email()['body_text']

def test_ip_is_404_for_unknown_code(self):
assert self.client.GxT('/~bob/identities/XX', auth_as='bob').code == 404

def test_ip_is_302_if_no_verified_email(self):
response = self.client.GxT('/~alice/identities/TT', auth_as='alice')
assert response.code == 302
assert response.headers['Location'] == '/about/me/emails/'


def test_ip_is_200_for_third_identity(self):
self.verify('bob', 'TT', 'US')
assert self.client.GET('/~bob/identities/US', auth_as='bob').code == 200

def test_ip_is_302_for_fourth_identity(self):
self.verify('bob', 'TT', 'US', 'GB')
assert self.client.GxT('/~bob/identities/CA', auth_as='bob').code == 302

def test_ip_is_302_for_fifth_identities(self):
self.verify('bob', 'TT', 'US', 'GB', 'GH')
assert self.client.GxT('/~bob/identities/CA', auth_as='bob').code == 302

def test_but_ip_always_loads_for_own_identity(self):
self.verify('bob', 'TT', 'US', 'GB', 'GH')
assert self.client.GET('/~bob/identities/TT', auth_as='bob').code == 200

def test_ip_always_loads_for_own_identity_even_if_unverified(self):
self.verify('bob', 'US', 'GB', 'GH')
self.identify('bob', 'TT')
assert self.client.GET('/~bob/identities/TT', auth_as='bob').code == 200


def test_ip_removes_identity(self):
bob = self.verify('bob', 'TT')
assert len(bob.list_identity_metadata()) == 1
data = {'action': 'remove'}
assert self.client.PxST('/~bob/identities/TT', auth_as='bob', data=data).code == 302
assert len(bob.list_identity_metadata()) == 0

def test_ip_stores_identity(self):
bob = Participant.from_username('bob')
assert len(bob.list_identity_metadata()) == 1
data = { 'id_type': ''
, 'id_number': ''
, 'legal_name': 'Bobsworth B. Bobbleton, IV'
, 'dob': ''
, 'address_1': ''
, 'address_2': ''
, 'city': ''
, 'region': ''
, 'postcode': ''
, 'action': 'store'
}
assert self.client.PxST('/~bob/identities/US', auth_as='bob', data=data).code == 302
assert len(bob.list_identity_metadata()) == 2
info = bob.retrieve_identity_info(Country.from_code('US').id)
assert info['legal_name'] == 'Bobsworth B. Bobbleton, IV'

def test_ip_validates_action(self):
bob = Participant.from_username('bob')
assert len(bob.list_identity_metadata()) == 1
data = {'action': 'cheese'}
assert self.client.PxST('/~bob/identities/TT', auth_as='bob', data=data).code == 400
assert len(bob.list_identity_metadata()) == 1
1 change: 1 addition & 0 deletions tests/py/test_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def browse(self, setup=None, **kw):
.replace('/%platform/', '/github/') \
.replace('/%user_name/', '/gratipay/') \
.replace('/%membername', '/alan') \
.replace('/%country', '/TT') \
.replace('/%exchange_id.int', '/%s' % exchange_id) \
.replace('/%redirect_to', '/giving') \
.replace('/%endpoint', '/public') \
Expand Down
3 changes: 2 additions & 1 deletion www/assets/gratipay.css.spt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
@import "scss/components/js-edit";
@import "scss/components/linear_gradient";
@import "scss/components/loading-indicators";
@import "scss/components/long-form";
@import "scss/components/memberships";
@import "scss/components/nav";
@import "scss/components/payments-by";
Expand All @@ -59,11 +60,11 @@

@import "scss/pages/homepage";
@import "scss/pages/history";
@import "scss/pages/identities";
@import "scss/pages/team";
@import "scss/pages/profile-edit";
@import "scss/pages/giving";
@import "scss/pages/settings";
@import "scss/pages/cc-ba";
@import "scss/pages/on-confirm";
@import "scss/pages/search";
@import "scss/pages/hall-of-fame";
Loading

0 comments on commit da4cace

Please sign in to comment.