Skip to content

Commit e456869

Browse files
committed
feat(users): add follow/unfollow API
1 parent e06c51b commit e456869

File tree

4 files changed

+152
-0
lines changed

4 files changed

+152
-0
lines changed

docs/gl_objects/users.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ Activate/Deactivate a user::
6767
user.activate()
6868
user.deactivate()
6969

70+
Follow/Unfollow a user::
71+
72+
user.follow()
73+
user.unfollow()
74+
7075
Set the avatar image for a user::
7176

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

8590
user.identityproviders.delete('oauth2_generic')
8691

92+
Get the followers of a user
93+
94+
user.followers_users.list()
95+
96+
Get the followings of a user
97+
98+
user.following_users.list()
99+
100+
87101
User custom attributes
88102
======================
89103

gitlab/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,14 @@ class GitlabLicenseError(GitlabOperationError):
261261
pass
262262

263263

264+
class GitlabFollowError(GitlabOperationError):
265+
pass
266+
267+
268+
class GitlabUnfollowError(GitlabOperationError):
269+
pass
270+
271+
264272
def on_http_error(error):
265273
"""Manage GitlabHttpError exceptions.
266274

gitlab/tests/objects/test_users.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,72 @@ def resp_delete_user_identity(no_content):
108108
yield rsps
109109

110110

111+
@pytest.fixture
112+
def resp_follow_unfollow():
113+
user = {
114+
"id": 1,
115+
"username": "john_smith",
116+
"name": "John Smith",
117+
"state": "active",
118+
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
119+
"web_url": "http://localhost:3000/john_smith",
120+
}
121+
with responses.RequestsMock() as rsps:
122+
rsps.add(
123+
method=responses.POST,
124+
url="http://localhost/api/v4/users/1/follow",
125+
json=user,
126+
content_type="application/json",
127+
status=201,
128+
)
129+
rsps.add(
130+
method=responses.POST,
131+
url="http://localhost/api/v4/users/1/unfollow",
132+
json=user,
133+
content_type="application/json",
134+
status=201,
135+
)
136+
yield rsps
137+
138+
139+
@pytest.fixture
140+
def resp_followers_following():
141+
content = [
142+
{
143+
"id": 2,
144+
"name": "Lennie Donnelly",
145+
"username": "evette.kilback",
146+
"state": "active",
147+
"avatar_url": "https://www.gravatar.com/avatar/7955171a55ac4997ed81e5976287890a?s=80&d=identicon",
148+
"web_url": "http://127.0.0.1:3000/evette.kilback",
149+
},
150+
{
151+
"id": 4,
152+
"name": "Serena Bradtke",
153+
"username": "cammy",
154+
"state": "active",
155+
"avatar_url": "https://www.gravatar.com/avatar/a2daad869a7b60d3090b7b9bef4baf57?s=80&d=identicon",
156+
"web_url": "http://127.0.0.1:3000/cammy",
157+
},
158+
]
159+
with responses.RequestsMock() as rsps:
160+
rsps.add(
161+
method=responses.GET,
162+
url="http://localhost/api/v4/users/1/followers",
163+
json=content,
164+
content_type="application/json",
165+
status=200,
166+
)
167+
rsps.add(
168+
method=responses.GET,
169+
url="http://localhost/api/v4/users/1/following",
170+
json=content,
171+
content_type="application/json",
172+
status=200,
173+
)
174+
yield rsps
175+
176+
111177
def test_get_user(gl, resp_get_user):
112178
user = gl.users.get(1)
113179
assert isinstance(user, User)
@@ -135,3 +201,17 @@ def test_user_activate_deactivate(user, resp_activate):
135201

136202
def test_delete_user_identity(user, resp_delete_user_identity):
137203
user.identityproviders.delete("test_provider")
204+
205+
206+
def test_user_follow_unfollow(user, resp_follow_unfollow):
207+
user.follow()
208+
user.unfollow()
209+
210+
211+
def test_list_followers(user, resp_followers_following):
212+
followers = user.followers_users.list()
213+
followings = user.following_users.list()
214+
assert isinstance(followers[0], User)
215+
assert followers[0].id == 2
216+
assert isinstance(followings[0], User)
217+
assert followings[1].id == 4

gitlab/v4/objects/users.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ class User(SaveMixin, ObjectDeleteMixin, RESTObject):
106106
_managers = (
107107
("customattributes", "UserCustomAttributeManager"),
108108
("emails", "UserEmailManager"),
109+
("followers_users", "UserFollowersManager"),
110+
("following_users", "UserFollowingManager"),
109111
("events", "UserEventManager"),
110112
("gpgkeys", "UserGPGKeyManager"),
111113
("identityproviders", "UserIdentityProviderManager"),
@@ -137,6 +139,42 @@ def block(self, **kwargs):
137139
self._attrs["state"] = "blocked"
138140
return server_data
139141

142+
@cli.register_custom_action("User")
143+
@exc.on_http_error(exc.GitlabFollowError)
144+
def follow(self, **kwargs):
145+
"""Follow the user.
146+
147+
Args:
148+
**kwargs: Extra options to send to the server (e.g. sudo)
149+
150+
Raises:
151+
GitlabAuthenticationError: If authentication is not correct
152+
GitlabFollowError: If the user could not be followed
153+
154+
Returns:
155+
dict: The new object data (*not* a RESTObject)
156+
"""
157+
path = "/users/%s/follow" % self.id
158+
return self.manager.gitlab.http_post(path, **kwargs)
159+
160+
@cli.register_custom_action("User")
161+
@exc.on_http_error(exc.GitlabUnfollowError)
162+
def unfollow(self, **kwargs):
163+
"""Unfollow the user.
164+
165+
Args:
166+
**kwargs: Extra options to send to the server (e.g. sudo)
167+
168+
Raises:
169+
GitlabAuthenticationError: If authentication is not correct
170+
GitlabUnfollowError: If the user could not be followed
171+
172+
Returns:
173+
dict: The new object data (*not* a RESTObject)
174+
"""
175+
path = "/users/%s/unfollow" % self.id
176+
return self.manager.gitlab.http_post(path, **kwargs)
177+
140178
@cli.register_custom_action("User")
141179
@exc.on_http_error(exc.GitlabUnblockError)
142180
def unblock(self, **kwargs):
@@ -454,3 +492,15 @@ def list(self, **kwargs):
454492
else:
455493
path = "/users/%s/projects" % kwargs["user_id"]
456494
return ListMixin.list(self, path=path, **kwargs)
495+
496+
497+
class UserFollowersManager(ListMixin, RESTManager):
498+
_path = "/users/%(user_id)s/followers"
499+
_obj_cls = User
500+
_from_parent_attrs = {"user_id": "id"}
501+
502+
503+
class UserFollowingManager(ListMixin, RESTManager):
504+
_path = "/users/%(user_id)s/following"
505+
_obj_cls = User
506+
_from_parent_attrs = {"user_id": "id"}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy