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' %} - {{ _("Under Review") }} | - {% elif team.status == 'rejected' %} - {{ _("Rejected") }} | - {% endif %} + + {% 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

- {{ status_icons[team.status]|safe }} - {{ i18ned_statuses[team.status] }} + + {{ status_icons[team.status]|safe }} + {{ i18ned_statuses[team.status] }} + · {{ _("created {ago}", ago=to_age(team.ctime, add_direction=True)) }} diff --git a/www/new.spt b/www/new.spt index b4ddebaf53..c29b57240b 100644 --- a/www/new.spt +++ b/www/new.spt @@ -44,6 +44,14 @@ still_migrating = delta > 0 height: 200px; } +
diff --git a/www/teams/create.json.spt b/www/teams/create.json.spt index 1bb8df0783..48432f112c 100644 --- a/www/teams/create.json.spt +++ b/www/teams/create.json.spt @@ -54,9 +54,12 @@ if request.method == 'POST': fields['slug'] = slugize(fields['name']) try: - Team.insert(user.participant, **fields) + team = Team.insert(user.participant, **fields) except IntegrityError: raise Response(400, _("Sorry, there is already a team using '{}'.", fields['slug'])) + review_url = team.create_github_review_issue() + team.set_review_url(review_url) + [---] application/json via json_dump -{'email': user.participant.email_address} +{'review_url': review_url}