Skip to content

feat(users): add follow/unfollow API #1333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/gl_objects/users.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ Activate/Deactivate a user::
user.activate()
user.deactivate()

Follow/Unfollow a user::

user.follow()
user.unfollow()

Set the avatar image for a user::

# the avatar image can be passed as data (content of the file) or as a file
Expand All @@ -84,6 +89,15 @@ Delete an external identity by provider name::

user.identityproviders.delete('oauth2_generic')

Get the followers of a user

user.followers_users.list()

Get the followings of a user

user.following_users.list()


User custom attributes
======================

Expand Down
8 changes: 8 additions & 0 deletions gitlab/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,14 @@ class GitlabLicenseError(GitlabOperationError):
pass


class GitlabFollowError(GitlabOperationError):
pass


class GitlabUnfollowError(GitlabOperationError):
pass


def on_http_error(error):
"""Manage GitlabHttpError exceptions.
Expand Down
80 changes: 80 additions & 0 deletions gitlab/tests/objects/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,72 @@ def resp_delete_user_identity(no_content):
yield rsps


@pytest.fixture
def resp_follow_unfollow():
user = {
"id": 1,
"username": "john_smith",
"name": "John Smith",
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
"web_url": "http://localhost:3000/john_smith",
}
with responses.RequestsMock() as rsps:
rsps.add(
method=responses.POST,
url="http://localhost/api/v4/users/1/follow",
json=user,
content_type="application/json",
status=201,
)
rsps.add(
method=responses.POST,
url="http://localhost/api/v4/users/1/unfollow",
json=user,
content_type="application/json",
status=201,
)
yield rsps


@pytest.fixture
def resp_followers_following():
content = [
{
"id": 2,
"name": "Lennie Donnelly",
"username": "evette.kilback",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/7955171a55ac4997ed81e5976287890a?s=80&d=identicon",
"web_url": "http://127.0.0.1:3000/evette.kilback",
},
{
"id": 4,
"name": "Serena Bradtke",
"username": "cammy",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/a2daad869a7b60d3090b7b9bef4baf57?s=80&d=identicon",
"web_url": "http://127.0.0.1:3000/cammy",
},
]
with responses.RequestsMock() as rsps:
rsps.add(
method=responses.GET,
url="http://localhost/api/v4/users/1/followers",
json=content,
content_type="application/json",
status=200,
)
rsps.add(
method=responses.GET,
url="http://localhost/api/v4/users/1/following",
json=content,
content_type="application/json",
status=200,
)
yield rsps


def test_get_user(gl, resp_get_user):
user = gl.users.get(1)
assert isinstance(user, User)
Expand Down Expand Up @@ -135,3 +201,17 @@ def test_user_activate_deactivate(user, resp_activate):

def test_delete_user_identity(user, resp_delete_user_identity):
user.identityproviders.delete("test_provider")


def test_user_follow_unfollow(user, resp_follow_unfollow):
user.follow()
user.unfollow()


def test_list_followers(user, resp_followers_following):
followers = user.followers_users.list()
followings = user.following_users.list()
assert isinstance(followers[0], User)
assert followers[0].id == 2
assert isinstance(followings[0], User)
assert followings[1].id == 4
50 changes: 50 additions & 0 deletions gitlab/v4/objects/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ class User(SaveMixin, ObjectDeleteMixin, RESTObject):
_managers = (
("customattributes", "UserCustomAttributeManager"),
("emails", "UserEmailManager"),
("followers_users", "UserFollowersManager"),
("following_users", "UserFollowingManager"),
("events", "UserEventManager"),
("gpgkeys", "UserGPGKeyManager"),
("identityproviders", "UserIdentityProviderManager"),
Expand Down Expand Up @@ -137,6 +139,42 @@ def block(self, **kwargs):
self._attrs["state"] = "blocked"
return server_data

@cli.register_custom_action("User")
@exc.on_http_error(exc.GitlabFollowError)
def follow(self, **kwargs):
"""Follow the user.

Args:
**kwargs: Extra options to send to the server (e.g. sudo)

Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabFollowError: If the user could not be followed

Returns:
dict: The new object data (*not* a RESTObject)
"""
path = "/users/%s/follow" % self.id
return self.manager.gitlab.http_post(path, **kwargs)

@cli.register_custom_action("User")
@exc.on_http_error(exc.GitlabUnfollowError)
def unfollow(self, **kwargs):
"""Unfollow the user.

Args:
**kwargs: Extra options to send to the server (e.g. sudo)

Raises:
GitlabAuthenticationError: If authentication is not correct
GitlabUnfollowError: If the user could not be followed

Returns:
dict: The new object data (*not* a RESTObject)
"""
path = "/users/%s/unfollow" % self.id
return self.manager.gitlab.http_post(path, **kwargs)

@cli.register_custom_action("User")
@exc.on_http_error(exc.GitlabUnblockError)
def unblock(self, **kwargs):
Expand Down Expand Up @@ -454,3 +492,15 @@ def list(self, **kwargs):
else:
path = "/users/%s/projects" % kwargs["user_id"]
return ListMixin.list(self, path=path, **kwargs)


class UserFollowersManager(ListMixin, RESTManager):
_path = "/users/%(user_id)s/followers"
_obj_cls = User
_from_parent_attrs = {"user_id": "id"}


class UserFollowingManager(ListMixin, RESTManager):
_path = "/users/%(user_id)s/following"
_obj_cls = User
_from_parent_attrs = {"user_id": "id"}