diff --git a/defaults.env b/defaults.env
index 709f9bf3e8..78405f009b 100644
--- a/defaults.env
+++ b/defaults.env
@@ -76,4 +76,10 @@ GUNICORN_OPTS="--workers=1 --timeout=99999999"
MANDRILL_KEY=Phh_Lm3RdPT5blqOPY4dVQ
+# For testing Team review ticket posting
+# Set your own username and an access token in local.env
+TEAM_REVIEW_REPO=gratipay/test-gremlin
+TEAM_REVIEW_USERNAME=
+TEAM_REVIEW_TOKEN=
+
RAISE_SIGNIN_NOTIFICATIONS=no
diff --git a/emails/team-approved.spt b/emails/team-approved.spt
new file mode 100644
index 0000000000..8f6dab84ab
--- /dev/null
+++ b/emails/team-approved.spt
@@ -0,0 +1,18 @@
+{{ _("Team Application Approved!") }}
+[---] text/html
+{{ _( "We've approved your application for the '{team}' Team on Gratipay. For details, please refer to {a}our review ticket{_a}."
+ , team=team.name
+ , a=(''|safe).format(team.review_url)
+ , _a=''|safe
+ ) }}
+
+
+{{ _("Thanks for using Gratipay! :-)") }}
+[---] text/plain
+{{ _( "We've approved your application for the '{team}' Team on Gratipay. For details, please refer to our review ticket:"
+ , team=team.name
+ ) }}
+
+{{ team.review_url }}
+
+{{ _("Thanks for using Gratipay! :-)") }}
diff --git a/emails/team-rejected.spt b/emails/team-rejected.spt
new file mode 100644
index 0000000000..f0b0d7b7aa
--- /dev/null
+++ b/emails/team-rejected.spt
@@ -0,0 +1,13 @@
+{{ _("Team Application Rejected") }}
+[---] text/html
+{{ _( "We've rejected your application for the '{team}' Team on Gratipay. For details, please refer to {a}our review ticket{_a}."
+ , team=team.name
+ , a=(''|safe).format(team.review_url)
+ , _a=''|safe
+ ) }}
+[---] text/plain
+{{ _( "We've rejected your application for the '{team}' Team on Gratipay. For details, please refer to our review ticket:"
+ , team=team.name
+ ) }}
+
+{{ team.review_url }}
diff --git a/gratipay/main.py b/gratipay/main.py
index a2b5a4f301..1d481402f8 100644
--- a/gratipay/main.py
+++ b/gratipay/main.py
@@ -64,6 +64,7 @@
gratipay.wireup.base_url(website, env)
gratipay.wireup.secure_cookies(env)
gratipay.wireup.billing(env)
+gratipay.wireup.team_review(env)
gratipay.wireup.username_restrictions(website)
gratipay.wireup.load_i18n(website.project_root, tell_sentry)
gratipay.wireup.other_stuff(website, env)
diff --git a/gratipay/models/team.py b/gratipay/models/team.py
index 98a94f93b3..b9d5a346b6 100644
--- a/gratipay/models/team.py
+++ b/gratipay/models/team.py
@@ -1,6 +1,8 @@
"""Teams on Gratipay receive payments and distribute payroll.
"""
+import requests
from postgres.orm import Model
+from aspen import json, log
status_icons = { "unreviewed": "✋"
, "rejected": "❌"
@@ -68,6 +70,29 @@ def insert(cls, owner, **fields):
""", fields)
+
+ def create_github_review_issue(self):
+ """POST to GitHub, and return the URL of the new issue.
+ """
+ api_url = "https://api.github.com/repos/{}/issues".format(self.review_repo)
+ data = json.dumps({ "title": "review {}".format(self.name)
+ , "body": "https://gratipay.com/{}/".format(self.slug)
+ })
+ r = requests.post(api_url, auth=self.review_auth, data=data)
+ if r.status_code == 201:
+ out = r.json()['html_url']
+ else:
+ log(r.status_code)
+ log(r.text)
+ out = "https://github.com/gratipay/team-review/issues#error-{}".format(r.status_code)
+ return out
+
+
+ def set_review_url(self, review_url):
+ self.db.run("UPDATE teams SET review_url=%s WHERE id=%s", (review_url, self.id))
+ self.set_attributes(review_url=review_url)
+
+
def get_og_title(self):
out = self.name
receiving = self.receiving
diff --git a/gratipay/testing/__init__.py b/gratipay/testing/__init__.py
index d127dbbe32..257f9095f4 100644
--- a/gratipay/testing/__init__.py
+++ b/gratipay/testing/__init__.py
@@ -310,3 +310,30 @@ def get_tip(self, tipper, tippee):
class Foobar(Exception): pass
+
+
+def debug_http():
+ """Turns on debug logging for HTTP traffic. Happily, this includes VCR usage.
+
+ http://stackoverflow.com/a/16630836
+
+ """
+ import logging
+
+ # These two lines enable debugging at httplib level
+ # (requests->urllib3->http.client) You will see the REQUEST, including
+ # HEADERS and DATA, and RESPONSE with HEADERS but without DATA. The
+ # only thing missing will be the response.body which is not logged.
+ try:
+ import http.client as http_client
+ except ImportError:
+ # Python 2
+ import httplib as http_client
+ http_client.HTTPConnection.debuglevel = 1
+
+ # You must initialize logging, otherwise you'll not see debug output.
+ logging.basicConfig()
+ logging.getLogger().setLevel(logging.DEBUG)
+ requests_log = logging.getLogger("requests.packages.urllib3")
+ requests_log.setLevel(logging.DEBUG)
+ requests_log.propagate = True
diff --git a/gratipay/wireup.py b/gratipay/wireup.py
index 38049ed3a7..798fa05048 100644
--- a/gratipay/wireup.py
+++ b/gratipay/wireup.py
@@ -84,6 +84,12 @@ def billing(env):
env.braintree_private_key
)
+
+def team_review(env):
+ Team.review_repo = env.team_review_repo
+ Team.review_auth = (env.team_review_username, env.team_review_token)
+
+
def username_restrictions(website):
gratipay.RESTRICTED_USERNAMES = os.listdir(website.www_root)
@@ -402,6 +408,9 @@ def env():
LOG_METRICS = is_yesish,
INCLUDE_PIWIK = is_yesish,
MANDRILL_KEY = unicode,
+ TEAM_REVIEW_REPO = unicode,
+ TEAM_REVIEW_USERNAME = unicode,
+ TEAM_REVIEW_TOKEN = unicode,
RAISE_SIGNIN_NOTIFICATIONS = is_yesish,
# This is used in our Procfile. (PORT is also used but is provided by
diff --git a/js/gratipay/new_team.js b/js/gratipay/new_team.js
index 11b9bd3389..ab48061108 100644
--- a/js/gratipay/new_team.js
+++ b/js/gratipay/new_team.js
@@ -21,10 +21,10 @@ Gratipay.new_team.submitForm = function (e) {
data: data,
dataType: 'json',
success: function (d) {
- $('form').html( "
Thank you! We will follow up shortly with an email to " - + d.email + ". Please email " - + "us with any questions.
" - ) + $('a.review_url').attr('href', d.review_url).text(d.review_url); + $('form').slideUp(500, function() { + $('.application-complete').slideDown(250); + }); }, error: [Gratipay.error, function () { $input.prop('disable', false); }] }); diff --git a/scss/pages/homepage.scss b/scss/pages/homepage.scss index 78f1470d63..52939e1026 100644 --- a/scss/pages/homepage.scss +++ b/scss/pages/homepage.scss @@ -44,6 +44,11 @@ border: 1px solid $light-brown; border-style: solid none; + a { + color: $medium-gray; + position: relative; + z-index: 2; + } a:hover { background: none !important; color: $green !important; @@ -65,11 +70,6 @@ min-height: 48px; padding: 30px 64px 0 0; left: 0; - .owner a { - color: $medium-gray; - position: relative; - z-index: 2; - } span { white-space: nowrap; } diff --git a/sql/branch.sql b/sql/branch.sql new file mode 100644 index 0000000000..945329ec57 --- /dev/null +++ b/sql/branch.sql @@ -0,0 +1,3 @@ +BEGIN; + ALTER TABLE teams ADD COLUMN review_url text DEFAULT NULL; +END; diff --git a/tests/py/fixtures/TestNewTeams.yml b/tests/py/fixtures/TestNewTeams.yml new file mode 100644 index 0000000000..b9201243cf --- /dev/null +++ b/tests/py/fixtures/TestNewTeams.yml @@ -0,0 +1,26 @@ +interactions: +- request: + body: '{"body": "https://gratipay.com/TheATeam/", "title": "review The A Team"}' + headers: {} + method: POST + uri: https://api.github.com:443/repos/gratipay/review/issues + response: + body: {string: !!python/unicode '{"url":"https://api.github.com/repos/gratipay/review/issues/2","labels_url":"https://api.github.com/repos/gratipay/review/issues/2/labels{/name}","comments_url":"https://api.github.com/repos/gratipay/review/issues/2/comments","events_url":"https://api.github.com/repos/gratipay/review/issues/2/events","html_url":"https://github.com/gratipay/review/issues/2","id":89539256,"number":2,"title":"review + The A Team","user":{"login":"gratipay-gremlin","id":12961261,"avatar_url":"https://avatars.githubusercontent.com/u/12961261?v=3","gravatar_id":"","url":"https://api.github.com/users/gratipay-gremlin","html_url":"https://github.com/gratipay-gremlin","followers_url":"https://api.github.com/users/gratipay-gremlin/followers","following_url":"https://api.github.com/users/gratipay-gremlin/following{/other_user}","gists_url":"https://api.github.com/users/gratipay-gremlin/gists{/gist_id}","starred_url":"https://api.github.com/users/gratipay-gremlin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gratipay-gremlin/subscriptions","organizations_url":"https://api.github.com/users/gratipay-gremlin/orgs","repos_url":"https://api.github.com/users/gratipay-gremlin/repos","events_url":"https://api.github.com/users/gratipay-gremlin/events{/privacy}","received_events_url":"https://api.github.com/users/gratipay-gremlin/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":0,"created_at":"2015-06-19T11:19:55Z","updated_at":"2015-06-19T11:19:55Z","closed_at":null,"body":"https://gratipay.com/TheATeam/","closed_by":null}'} + headers: + access-control-allow-credentials: ['true'] + access-control-allow-origin: ['*'] + access-control-expose-headers: ['ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, + X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval'] + cache-control: ['private, max-age=60, s-maxage=60'] + content-length: ['1624'] + content-security-policy: [default-src 'none'] + content-type: [application/json; charset=utf-8] + etag: ['"38c0f12312600b8f603f62407c845381"'] + location: ['https://api.github.com/repos/gratipay/review/issues/2'] + status: [201 Created] + strict-transport-security: [max-age=31536000; includeSubdomains; preload] + vary: ['Accept, Authorization, Cookie, X-GitHub-OTP', Accept-Encoding] + status: {code: 201, message: Created} +version: 1 diff --git a/tests/py/fixtures/TestTeams.yml b/tests/py/fixtures/TestTeams.yml new file mode 100644 index 0000000000..9176a0870c --- /dev/null +++ b/tests/py/fixtures/TestTeams.yml @@ -0,0 +1,77 @@ +interactions: +- request: + body: "{\n \"body\": \"https://gratipay.com/gratiteam/\",\n \"title\": \"review + Gratiteam\"\n}" + headers: {} + method: POST + uri: https://api.github.com:443/repos/gratipay/test-gremlin/issues + response: + body: {string: !!python/unicode '{"url":"https://api.github.com/repos/gratipay/test-gremlin/issues/9","labels_url":"https://api.github.com/repos/gratipay/test-gremlin/issues/9/labels{/name}","comments_url":"https://api.github.com/repos/gratipay/test-gremlin/issues/9/comments","events_url":"https://api.github.com/repos/gratipay/test-gremlin/issues/9/events","html_url":"https://github.com/gratipay/test-gremlin/issues/9","id":105293440,"number":9,"title":"review + Gratiteam","user":{"login":"lgtest","id":1775515,"avatar_url":"https://avatars.githubusercontent.com/u/1775515?v=3","gravatar_id":"","url":"https://api.github.com/users/lgtest","html_url":"https://github.com/lgtest","followers_url":"https://api.github.com/users/lgtest/followers","following_url":"https://api.github.com/users/lgtest/following{/other_user}","gists_url":"https://api.github.com/users/lgtest/gists{/gist_id}","starred_url":"https://api.github.com/users/lgtest/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lgtest/subscriptions","organizations_url":"https://api.github.com/users/lgtest/orgs","repos_url":"https://api.github.com/users/lgtest/repos","events_url":"https://api.github.com/users/lgtest/events{/privacy}","received_events_url":"https://api.github.com/users/lgtest/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":0,"created_at":"2015-09-08T03:26:30Z","updated_at":"2015-09-08T03:26:30Z","closed_at":null,"body":"https://gratipay.com/gratiteam/","closed_by":null}'} + headers: + access-control-allow-credentials: ['true'] + access-control-allow-origin: ['*'] + access-control-expose-headers: ['ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, + X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval'] + cache-control: ['private, max-age=60, s-maxage=60'] + content-length: ['1533'] + content-security-policy: [default-src 'none'] + content-type: [application/json; charset=utf-8] + etag: ['"fda883470b69e8f697fe0644a8b3e5dc"'] + location: ['https://api.github.com/repos/gratipay/test-gremlin/issues/9'] + status: [201 Created] + strict-transport-security: [max-age=31536000; includeSubdomains; preload] + vary: ['Accept, Authorization, Cookie, X-GitHub-OTP', Accept-Encoding] + status: {code: 201, message: Created} +- request: + body: "{\n \"body\": \"https://gratipay.com/gratiteam/\",\n \"title\": \"review + Gratiteam\"\n}" + headers: {} + method: POST + uri: https://api.github.com:443/repos/gratipay/test-gremlin/issues + response: + body: {string: !!python/unicode '{"url":"https://api.github.com/repos/gratipay/test-gremlin/issues/10","labels_url":"https://api.github.com/repos/gratipay/test-gremlin/issues/10/labels{/name}","comments_url":"https://api.github.com/repos/gratipay/test-gremlin/issues/10/comments","events_url":"https://api.github.com/repos/gratipay/test-gremlin/issues/10/events","html_url":"https://github.com/gratipay/test-gremlin/issues/10","id":105293441,"number":10,"title":"review + Gratiteam","user":{"login":"lgtest","id":1775515,"avatar_url":"https://avatars.githubusercontent.com/u/1775515?v=3","gravatar_id":"","url":"https://api.github.com/users/lgtest","html_url":"https://github.com/lgtest","followers_url":"https://api.github.com/users/lgtest/followers","following_url":"https://api.github.com/users/lgtest/following{/other_user}","gists_url":"https://api.github.com/users/lgtest/gists{/gist_id}","starred_url":"https://api.github.com/users/lgtest/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lgtest/subscriptions","organizations_url":"https://api.github.com/users/lgtest/orgs","repos_url":"https://api.github.com/users/lgtest/repos","events_url":"https://api.github.com/users/lgtest/events{/privacy}","received_events_url":"https://api.github.com/users/lgtest/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":0,"created_at":"2015-09-08T03:26:30Z","updated_at":"2015-09-08T03:26:30Z","closed_at":null,"body":"https://gratipay.com/gratiteam/","closed_by":null}'} + headers: + access-control-allow-credentials: ['true'] + access-control-allow-origin: ['*'] + access-control-expose-headers: ['ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, + X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval'] + cache-control: ['private, max-age=60, s-maxage=60'] + content-length: ['1539'] + content-security-policy: [default-src 'none'] + content-type: [application/json; charset=utf-8] + etag: ['"16006dd40973a34b57eaa9c47f389e18"'] + location: ['https://api.github.com/repos/gratipay/test-gremlin/issues/10'] + status: [201 Created] + strict-transport-security: [max-age=31536000; includeSubdomains; preload] + vary: ['Accept, Authorization, Cookie, X-GitHub-OTP', Accept-Encoding] + status: {code: 201, message: Created} +- request: + body: "{\n \"body\": \"https://gratipay.com/gratiteam/\",\n \"title\": \"review + Gratiteam\"\n}" + headers: {} + method: POST + uri: https://api.github.com:443/repos/gratipay/test-gremlin/issues + response: + body: {string: !!python/unicode '{"url":"https://api.github.com/repos/gratipay/test-gremlin/issues/11","labels_url":"https://api.github.com/repos/gratipay/test-gremlin/issues/11/labels{/name}","comments_url":"https://api.github.com/repos/gratipay/test-gremlin/issues/11/comments","events_url":"https://api.github.com/repos/gratipay/test-gremlin/issues/11/events","html_url":"https://github.com/gratipay/test-gremlin/issues/11","id":105293442,"number":11,"title":"review + Gratiteam","user":{"login":"lgtest","id":1775515,"avatar_url":"https://avatars.githubusercontent.com/u/1775515?v=3","gravatar_id":"","url":"https://api.github.com/users/lgtest","html_url":"https://github.com/lgtest","followers_url":"https://api.github.com/users/lgtest/followers","following_url":"https://api.github.com/users/lgtest/following{/other_user}","gists_url":"https://api.github.com/users/lgtest/gists{/gist_id}","starred_url":"https://api.github.com/users/lgtest/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lgtest/subscriptions","organizations_url":"https://api.github.com/users/lgtest/orgs","repos_url":"https://api.github.com/users/lgtest/repos","events_url":"https://api.github.com/users/lgtest/events{/privacy}","received_events_url":"https://api.github.com/users/lgtest/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":0,"created_at":"2015-09-08T03:26:31Z","updated_at":"2015-09-08T03:26:31Z","closed_at":null,"body":"https://gratipay.com/gratiteam/","closed_by":null}'} + headers: + access-control-allow-credentials: ['true'] + access-control-allow-origin: ['*'] + access-control-expose-headers: ['ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, + X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, + X-Poll-Interval'] + cache-control: ['private, max-age=60, s-maxage=60'] + content-length: ['1539'] + content-security-policy: [default-src 'none'] + content-type: [application/json; charset=utf-8] + etag: ['"f36a625d0f49b0e6555ec814383b5663"'] + location: ['https://api.github.com/repos/gratipay/test-gremlin/issues/11'] + status: [201 Created] + strict-transport-security: [max-age=31536000; includeSubdomains; preload] + vary: ['Accept, Authorization, Cookie, X-GitHub-OTP', Accept-Encoding] + status: {code: 201, message: Created} +version: 1 diff --git a/tests/py/test_teams.py b/tests/py/test_teams.py index 3d7a47cf88..06d5111719 100644 --- a/tests/py/test_teams.py +++ b/tests/py/test_teams.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import json +import mock import pytest from decimal import Decimal @@ -7,6 +9,9 @@ from gratipay.models.team import Team, AlreadyMigrated +REVIEW_URL = "https://github.com/gratipay/test-gremlin/issues/9" + + class TestTeams(Harness): valid_data = { @@ -41,12 +46,15 @@ def test_can_construct_from_id(self): assert team.name == 'The Enterprise' assert team.owner == 'picard' - def test_can_create_new_team(self): + @mock.patch('gratipay.models.team.Team.create_github_review_issue') + def test_can_create_new_team(self, cgri): + cgri.return_value = REVIEW_URL self.make_participant('alice', claimed_time='now', email_address='', last_paypal_result='') - self.post_new(dict(self.valid_data)) + r = self.post_new(dict(self.valid_data)) team = self.db.one("SELECT * FROM teams") assert team assert team.owner == 'alice' + assert json.loads(r.body)['review_url'] == team.review_url def test_all_fields_persist(self): self.make_participant('alice', claimed_time='now', email_address='', last_paypal_result='') @@ -55,8 +63,7 @@ def test_all_fields_persist(self): assert team.name == 'Gratiteam' assert team.homepage == 'http://gratipay.com/' assert team.product_or_service == 'We make widgets.' - assert team.onboarding_url == 'http://inside.gratipay.com/' - assert team.todo_url == 'https://github.com/gratipay' + assert team.review_url == REVIEW_URL def test_casing_of_urls_survives(self): self.make_participant('alice', claimed_time='now', email_address='', last_paypal_result='') diff --git a/www/%team/index.html.spt b/www/%team/index.html.spt index d6bbdfc65d..775360d52b 100644 --- a/www/%team/index.html.spt +++ b/www/%team/index.html.spt @@ -86,11 +86,15 @@ suppress_sidebar = not(team.is_approved or user.ADMIN) {% block content %}- {% if team.status == 'unreviewed' %} - + {% if team.status == 'approved' %} + | {{ _("Approved") }} + {% elif team.status == 'unreviewed' %} + {{ _("Under Review") }} + {% elif team.status == 'rejected' %} + {{ _("Rejected") }} + {% endif %} + {{ _("Homepage") }} diff --git a/www/%team/set-status.json.spt b/www/%team/set-status.json.spt index ba6fe7ca24..ee8bed3a6f 100644 --- a/www/%team/set-status.json.spt +++ b/www/%team/set-status.json.spt @@ -2,6 +2,7 @@ from aspen import Response from gratipay.models import add_event from gratipay.models.team import Team +from gratipay.models.participant import Participant [---] if not user.ADMIN: raise Response(403) @@ -36,5 +37,9 @@ with website.db.get_cursor() as c: action='set', values=dict(status=status) )) + if status in ('rejected', 'approved'): + owner = Participant.from_username(team.owner) + owner.send_email('team-'+status, team=team, include_unsubscribe=False) + [---] application/json via json_dump {"status": status} diff --git a/www/index.html.spt b/www/index.html.spt index a857b497ce..6bed8fdc42 100644 --- a/www/index.html.spt +++ b/www/index.html.spt @@ -104,8 +104,10 @@ suppress_welcome = 'suppress-welcome' in request.cookie
{{ _("Under Review") }} | - {% elif team.status == 'rejected' %} - {{ _("Rejected") }} | - {% endif %} +