diff --git a/github/Consts.py b/github/Consts.py index 4bf902e9..1cf582b8 100644 --- a/github/Consts.py +++ b/github/Consts.py @@ -81,5 +81,8 @@ mediaTypeLabelDescriptionSearchPreview = "application/vnd.github.symmetra-previe # 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" diff --git a/github/Organization.py b/github/Organization.py index 0756a65b..853b47e4 100644 --- a/github/Organization.py +++ b/github/Organization.py @@ -21,6 +21,7 @@ # Copyright 2018 Raihaan <31362124+res0nance@users.noreply.github.com> # # Copyright 2018 Tim Boring # # Copyright 2018 sfdye # +# Copyright 2018 Steve Kowalik # # # # This file is part of PyGithub. # # http://pygithub.readthedocs.io/ # @@ -52,6 +53,7 @@ import github.Event import github.Repository import github.NamedUser +import Consts class Organization(github.GithubObject.CompletableGithubObject): """ @@ -776,6 +778,37 @@ class Organization(github.GithubObject.CompletableGithubObject): 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 `_ + :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 `_ diff --git a/github/tests/Organization.py b/github/tests/Organization.py index 30356d55..baa786c2 100644 --- a/github/tests/Organization.py +++ b/github/tests/Organization.py @@ -34,6 +34,8 @@ # # ################################################################################ +import github + import Framework import datetime @@ -213,3 +215,35 @@ class Organization(Framework.TestCase): 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.' + } + ) diff --git a/github/tests/ReplayData/Organization.testInviteUserAsNonOwner.txt b/github/tests/ReplayData/Organization.testInviteUserAsNonOwner.txt new file mode 100644 index 00000000..22c58692 --- /dev/null +++ b/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."} + diff --git a/github/tests/ReplayData/Organization.testInviteUserByEmail.txt b/github/tests/ReplayData/Organization.testInviteUserByEmail.txt new file mode 100644 index 00000000..f2e8ddb0 --- /dev/null +++ b/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} diff --git a/github/tests/ReplayData/Organization.testInviteUserByName.txt b/github/tests/ReplayData/Organization.testInviteUserByName.txt new file mode 100644 index 00000000..dc490155 --- /dev/null +++ b/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} diff --git a/github/tests/ReplayData/Organization.testInviteUserWithBoth.txt b/github/tests/ReplayData/Organization.testInviteUserWithBoth.txt new file mode 100644 index 00000000..1af92e06 --- /dev/null +++ b/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"} diff --git a/github/tests/ReplayData/Organization.testInviteUserWithRoleAndTeam.txt b/github/tests/ReplayData/Organization.testInviteUserWithRoleAndTeam.txt new file mode 100644 index 00000000..ca4d7946 --- /dev/null +++ b/github/tests/ReplayData/Organization.testInviteUserWithRoleAndTeam.txt @@ -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}