Skip to content

Commit

Permalink
Implement Organization.invite_user (#880)
Browse files Browse the repository at this point in the history
Utilize the API preview for the organization invite system to allow
organization owners to invite outside colloborators either via passing
in the user directly, or their email address.
    
Fixes #851
  • Loading branch information
s-t-e-v-e-n-k authored and sfdye committed Aug 28, 2018
1 parent ef16702 commit eb80564
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 0 deletions.
3 changes: 3 additions & 0 deletions github/Consts.py
Expand Up @@ -81,5 +81,8 @@
# https://developer.github.com/changes/2018-01-10-lock-reason-api-preview/
mediaTypeLockReasonPreview = "application/vnd.github.sailor-v-preview+json"

# https://developer.github.com/changes/2018-01-25-organization-invitation-api-preview/
mediaTypeOrganizationInvitationPreview = "application/vnd.github.dazzler-preview+json"

# https://developer.github.com/changes/2018-03-16-protected-branches-required-approving-reviews/
mediaTypeRequireMultipleApprovingReviews = "application/vnd.github.luke-cage-preview+json"
33 changes: 33 additions & 0 deletions github/Organization.py
Expand Up @@ -21,6 +21,7 @@
# Copyright 2018 Raihaan <31362124+res0nance@users.noreply.github.com> #
# Copyright 2018 Tim Boring <tboring@hearst.com> #
# Copyright 2018 sfdye <tsfdye@gmail.com> #
# Copyright 2018 Steve Kowalik <steven@wedontsleep.org> #
# #
# This file is part of PyGithub. #
# http://pygithub.readthedocs.io/ #
Expand Down Expand Up @@ -52,6 +53,7 @@
import github.Repository
import github.NamedUser

import Consts

class Organization(github.GithubObject.CompletableGithubObject):
"""
Expand Down Expand Up @@ -776,6 +778,37 @@ def get_teams(self):
None
)

def invite_user(self, user=github.GithubObject.NotSet, email=github.GithubObject.NotSet, role=github.GithubObject.NotSet, teams=github.GithubObject.NotSet):
"""
:calls: `POST /orgs/:org/invitations <http://developer.github.com/v3/orgs/members>`_
:param user: :class:`github.NamedUser.NamedUser`
:param email: string
:param role: string
:param teams: array of :class:`github.Team.Team`
:rtype: None
"""
assert user is github.GithubObject.NotSet or isinstance(user, github.NamedUser.NamedUser), user
assert email is github.GithubObject.NotSet or isinstance(email, (str, unicode)), email
assert (email is github.GithubObject.NotSet) ^ (user is github.GithubObject.NotSet), "specify only one of email or user"
parameters = {}
if user is not github.GithubObject.NotSet:
parameters["invitee_id"] = user.id
elif email is not github.GithubObject.NotSet:
parameters["email"] = email
if role is not github.GithubObject.NotSet:
assert isinstance(role, (str, unicode)), role
assert role in ['admin', 'direct_member', 'billing_manager']
parameters["role"] = role
if teams is not github.GithubObject.NotSet:
assert all(isinstance(team, github.Team.Team) for team in teams)
parameters["team_ids"] = [t.id for t in teams]
headers, data = self._requester.requestJsonAndCheck(
"POST",
self.url + "/invitations",
headers={'Accept': Consts.mediaTypeOrganizationInvitationPreview},
input=parameters
)

def has_in_members(self, member):
"""
:calls: `GET /orgs/:org/members/:user <http://developer.github.com/v3/orgs/members>`_
Expand Down
34 changes: 34 additions & 0 deletions github/tests/Organization.py
Expand Up @@ -34,6 +34,8 @@
# #
################################################################################

import github

import Framework

import datetime
Expand Down Expand Up @@ -213,3 +215,35 @@ def testCreateFork(self):
pygithub = self.g.get_user("jacquev6").get_repo("PyGithub")
repo = self.org.create_fork(pygithub)
self.assertEqual(repo.url, "https://api.github.com/repos/BeaverSoftware/PyGithub")

def testInviteUserWithNeither(self):
with self.assertRaises(AssertionError) as raisedexp:
self.org.invite_user()
self.assertEqual("specify only one of email or user", str(raisedexp.exception))

def testInviteUserWithBoth(self):
jacquev6 = self.g.get_user('jacquev6')
with self.assertRaises(AssertionError) as raisedexp:
self.org.invite_user(email='foo', user=jacquev6)
self.assertEqual("specify only one of email or user", str(raisedexp.exception))

def testInviteUserByName(self):
jacquev6 = self.g.get_user('jacquev6')
self.org.invite_user(user=jacquev6)

def testInviteUserByEmail(self):
self.org.invite_user(email='foo@example.com')

def testInviteUserWithRoleAndTeam(self):
team = self.org.create_team("Team created by PyGithub")
self.org.invite_user(email='foo@example.com', role='billing_manager', teams=[team])

def testInviteUserAsNonOwner(self):
with self.assertRaises(github.GithubException) as raisedexp:
self.org.invite_user(email='bar@example.com')
self.assertEqual(raisedexp.exception.status, 403)
self.assertEqual(raisedexp.exception.data, {
u'documentation_url': u'https://developer.github.com/v3/orgs/members/#create-organization-invitation',
u'message': u'You must be an admin to create an invitation to an organization.'
}
)
11 changes: 11 additions & 0 deletions github/tests/ReplayData/Organization.testInviteUserAsNonOwner.txt
@@ -0,0 +1,11 @@
https
POST
api.github.com
None
/orgs/BeaverSoftware/invitations
{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json', 'Accept': 'application/vnd.github.dazzler-preview+json'}
{"email": "bar@example.com"}
403
[('status', '403 Forbidden'), ('x-ratelimit-remaining', '4980'), ('content-length', '37'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"fb43ecb6e2f75e3940aa8e9edc5ed691"'), ('date', 'Sun, 26 Aug 2018 01:19:54 GMT'), ('content-type', 'application/json; charset=utf-8')]
{"documentation_url": "https://developer.github.com/v3/orgs/members/#create-organization-invitation", "message": "You must be an admin to create an invitation to an organization."}

10 changes: 10 additions & 0 deletions github/tests/ReplayData/Organization.testInviteUserByEmail.txt
@@ -0,0 +1,10 @@
https
POST
api.github.com
None
/orgs/BeaverSoftware/invitations
{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json', 'Accept': 'application/vnd.github.dazzler-preview+json'}
{"email": "foo@example.com"}
201
[('content-length', '1273'), ('x-runtime-rack', '0.071768'), ('vary', 'Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding'), ('x-oauth-scopes', 'admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, write:discussion'), ('x-xss-protection', '1; mode=block'), ('x-content-type-options', 'nosniff'), ('x-accepted-oauth-scopes', 'admin:org'), ('etag', '"a7e7e69aa42ed0d2132dbfcaa032dbf7"'), ('cache-control', 'private, max-age=60, s-maxage=60'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('status', '201 Created'), ('x-ratelimit-remaining', '4998'), ('x-github-media-type', 'github.dazzler-preview; format=json'), ('access-control-expose-headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('x-github-request-id', 'A542:171F:1945733:1F92438:5B822DC1'), ('date', 'Sun, 26 Aug 2018 04:34:24 GMT'), ('access-control-allow-origin', '*'), ('content-security-policy', "default-src 'none'"), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('server', 'GitHub.com'), ('x-ratelimit-limit', '5000'), ('x-frame-options', 'deny'), ('content-type', 'application/json; charset=utf-8'), ('x-ratelimit-reset', '1535261649')]
{"created_at":"2018-08-26T04:15:26Z","email":"foo@example.com","invitation_teams_url":"https://api.github.com/organizations/42690536/invitations/12423419/teams","node_id":"MDIyOk9yZ2FuaXphdGlvbkludml0YXRpb24xMjQyMzQxOQ==","role":"direct_member","team_count":0,"login":null,"inviter":{"following_url":"https://api.github.com/users/jacquev6/following{/other_user}","events_url":"https://api.github.com/users/jacquev6/events{/privacy}","avatar_url":"https://avatars0.githubusercontent.com/u/327146?v=4","url":"https://api.github.com/users/jacquev6","gists_url":"https://api.github.com/users/jacquev6/gists{/gist_id}","html_url":"https://github.com/jacquev6","subscriptions_url":"https://api.github.com/users/jacquev6/subscriptions","node_id":"MDQ6VXNlcjE1MjI1MDU5","repos_url":"https://api.github.com/users/jacquev6/repos","received_events_url":"https://api.github.com/users/jacquev6/received_events","gravatar_id":"","starred_url":"https://api.github.com/users/jacquev6/starred{/owner}{/repo}","site_admin":false,"login":"jacquev6","type":"User","id":327146,"followers_url":"https://api.github.com/users/jacquev6/followers","organizations_url":"https://api.github.com/users/jacquev6/orgs"},"id":12423419}
21 changes: 21 additions & 0 deletions github/tests/ReplayData/Organization.testInviteUserByName.txt
@@ -0,0 +1,21 @@
https
GET
api.github.com
None
/users/jacquev6
{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
None
200
[('status', '200 OK'), ('x-ratelimit-remaining', '4962'), ('content-length', '801'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"fc78d67f262cad756e42354c78ecea4e"'), ('date', 'Tue, 28 Aug 2018 00:16:42 GMT'), ('content-type', 'application/json; charset=utf-8')]
{"public_repos":11,"type":"User","disk_usage":17080,"hireable":false,"blog":"http://vincent-jacques.net","url":"https://api.github.com/users/jacquev6","bio":"","plan":{"collaborators":1,"private_repos":5,"name":"micro","space":614400},"avatar_url":"https://secure.gravatar.com/avatar/b68de5ae38616c296fa345d2b9df2225?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png","total_private_repos":5,"public_gists":2,"company":"Criteo","gravatar_id":"b68de5ae38616c296fa345d2b9df2225","login":"jacquev6","owned_private_repos":5,"private_gists":5,"collaborators":0,"email":"vincent@vincent-jacques.net","followers":13,"name":"Vincent Jacques","created_at":"2010-07-09T06:10:06Z","location":"Paris, France","id":327146,"following":24,"html_url":"https://github.com/jacquev6"}

https
POST
api.github.com
None
/orgs/BeaverSoftware/invitations
{'Authorization': 'Basic login_and_password_removed', 'Content-Type': 'application/json', 'User-Agent': 'PyGithub/Python', 'Accept': 'application/vnd.github.dazzler-preview+json'}
{"invitee_id": 327146}
201
[('content-length', '1275'), ('x-runtime-rack', '0.117213'), ('vary', 'Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding'), ('x-oauth-scopes', 'admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, write:discussion'), ('x-xss-protection', '1; mode=block'), ('x-content-type-options', 'nosniff'), ('x-accepted-oauth-scopes', 'admin:org'), ('etag', '"25cb4d8e0275889661b12ee12785894c"'), ('cache-control', 'private, max-age=60, s-maxage=60'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('status', '201 Created'), ('x-ratelimit-remaining', '4990'), ('x-github-media-type', 'github.dazzler-preview; format=json'), ('access-control-expose-headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('x-github-request-id', 'EA96:5917:ED3378:133B5C8:5B849484'), ('date', 'Tue, 28 Aug 2018 00:17:45 GMT'), ('access-control-allow-origin', '*'), ('content-security-policy', "default-src 'none'"), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('server', 'GitHub.com'), ('x-ratelimit-limit', '5000'), ('x-frame-options', 'deny'), ('content-type', 'application/json; charset=utf-8'), ('x-ratelimit-reset', '1535417380')]
{"created_at":"2018-08-28T00:17:45Z","email":"vincent@vincent-jacques.net","invitation_teams_url":"https://api.github.com/organizations/42733195/invitations/12434153/teams","node_id":"MDIyOk9yZ2FuaXphdGlvbkludml0YXRpb24xMjQzNDE1Mw==","role":"direct_member","team_count":0,"login":"jacquev6","inviter":{"following_url":"https://api.github.com/users/jacquev6/following{/other_user}","events_url":"https://api.github.com/users/jacquev6/events{/privacy}","avatar_url":"https://avatars0.githubusercontent.com/u/327146?v=4","url":"https://api.github.com/users/jacquev6","gists_url":"https://api.github.com/users/jacquev6/gists{/gist_id}","html_url":"https://github.com/jacquev6","subscriptions_url":"https://api.github.com/users/jacquev6/subscriptions","node_id":"MDQ6VXNlcjE1MjI1MDU5","repos_url":"https://api.github.com/users/jacquev6/repos","received_events_url":"https://api.github.com/users/jacquev6/received_events","gravatar_id":"","starred_url":"https://api.github.com/users/jacquev6/starred{/owner}{/repo}","site_admin":false,"login":"jacquev6","type":"User","id":327146,"followers_url":"https://api.github.com/users/jacquev6/followers","organizations_url":"https://api.github.com/users/jacquev6/orgs"},"id":12434153}
10 changes: 10 additions & 0 deletions github/tests/ReplayData/Organization.testInviteUserWithBoth.txt
@@ -0,0 +1,10 @@
https
GET
api.github.com
None
/users/jacquev6
{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
None
200
[('status', '200 OK'), ('x-ratelimit-remaining', '4962'), ('content-length', '801'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"fc78d67f262cad756e42354c78ecea4e"'), ('date', 'Tue, 28 Aug 2018 00:16:42 GMT'), ('content-type', 'application/json; charset=utf-8')]
{"public_repos":11,"type":"User","disk_usage":17080,"hireable":false,"blog":"http://vincent-jacques.net","url":"https://api.github.com/users/jacquev6","bio":"","plan":{"collaborators":1,"private_repos":5,"name":"micro","space":614400},"avatar_url":"https://secure.gravatar.com/avatar/b68de5ae38616c296fa345d2b9df2225?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png","total_private_repos":5,"public_gists":2,"company":"Criteo","gravatar_id":"b68de5ae38616c296fa345d2b9df2225","login":"jacquev6","owned_private_repos":5,"private_gists":5,"collaborators":0,"email":"vincent@vincent-jacques.net","followers":13,"name":"Vincent Jacques","created_at":"2010-07-09T06:10:06Z","location":"Paris, France","id":327146,"following":24,"html_url":"https://github.com/jacquev6"}
@@ -0,0 +1,21 @@
https
POST
api.github.com
None
/orgs/BeaverSoftware/teams
{'Content-Type': 'application/json', 'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
{"name": "Team created by PyGithub"}
201
[('status', '201 Created'), ('x-ratelimit-remaining', '4988'), ('content-length', '145'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"189a318993cde3e040f2efb4f634f8a8"'), ('date', 'Sat, 26 May 2012 20:58:53 GMT'), ('content-type', 'application/json; charset=utf-8'), ('location', 'https://api.github.com/teams/189850')]
{"url":"https://api.github.com/teams/189850","members_count":0,"repos_count":0,"name":"Team created by PyGithub","permission":"pull","id":189850}

https
POST
api.github.com
None
/orgs/BeaverSoftware/invitations
{'Content-Type': 'application/json', 'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python', 'Accept': 'application/vnd.github.dazzler-preview+json'}
{"email": "foo@example.com", "role": "billing_manager", "team_ids": [189850]}
201
[('content-length', '1278'), ('x-runtime-rack', '0.122927'), ('vary', 'Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding'), ('x-oauth-scopes', 'admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, write:discussion'), ('x-xss-protection', '1; mode=block'), ('x-content-type-options', 'nosniff'), ('x-accepted-oauth-scopes', 'admin:org'), ('etag', '"956a9bf38063ecf225e5970155c6022a"'), ('cache-control', 'private, max-age=60, s-maxage=60'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('status', '201 Created'), ('x-ratelimit-remaining', '4994'), ('x-github-media-type', 'github.dazzler-preview; format=json'), ('access-control-expose-headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('x-github-request-id', 'DA68:171E:16B488C:1C7BDFC:5B8232A3'), ('date', 'Sun, 26 Aug 2018 04:55:00 GMT'), ('access-control-allow-origin', '*'), ('content-security-policy', "default-src 'none'"), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('server', 'GitHub.com'), ('x-ratelimit-limit', '5000'), ('x-frame-options', 'deny'), ('content-type', 'application/json; charset=utf-8'), ('x-ratelimit-reset', '1535261649')]
{"created_at":"2018-08-26T04:55:00Z","email":"foo@example.com","invitation_teams_url":"https://api.github.com/organizations/32695636/invitations/12325953/teams","node_id":"MDIyOk9yZ2FuaXphdGlvbkludml0YXRpb24xMjQyMzQ1Mw==","role":"billing_manager","team_count":1,"login":null,"inviter":{"following_url":"https://api.github.com/users/jacquev6/following{/other_user}","events_url":"https://api.github.com/users/jacquev6/events{/privacy}","avatar_url":"https://avatars0.githubusercontent.com/u/327146?v=4","url":"https://api.github.com/users/jacquev6","gists_url":"https://api.github.com/users/jacquev6/gists{/gist_id}","html_url":"https://github.com/jacquev6","subscriptions_url":"https://api.github.com/users/jacquev6/subscriptions","node_id":"MDQ6VXNlcjE1MjI1MDU5","repos_url":"https://api.github.com/users/jacquev6/repos","received_events_url":"https://api.github.com/users/jacquev6/received_events","gravatar_id":"","starred_url":"https://api.github.com/users/jacquev6/starred{/owner}{/repo}","site_admin":false,"login":"jacquev6","type":"User","id":327146,"followers_url":"https://api.github.com/users/jacquev6/followers","organizations_url":"https://api.github.com/users/jacquev6/orgs"},"id":12423453}

0 comments on commit eb80564

Please sign in to comment.