Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Return total number of users and profile attributes in admin users endpoint #6881

Merged
merged 3 commits into from
Apr 28, 2020
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
1 change: 1 addition & 0 deletions changelog.d/6881.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Return total number of users and profile attributes in admin users endpoint. Contributed by Awesome Technologies Innovationslabor GmbH.
11 changes: 8 additions & 3 deletions docs/admin_api/user_admin_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,22 @@ It returns a JSON body like the following:
"is_guest": 0,
"admin": 0,
"user_type": null,
"deactivated": 0
"deactivated": 0,
"displayname": <User One>,
"avatar_url": null
}, {
"name": "<user_id2>",
"password_hash": "<password_hash2>",
"is_guest": 0,
"admin": 1,
"user_type": null,
"deactivated": 0
"deactivated": 0,
"displayname": <User Two>,
"avatar_url": "<avatar_url>"
}
],
"next_token": "100"
"next_token": "100",
"total": 200
}


Expand Down
8 changes: 4 additions & 4 deletions synapse/rest/admin/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,10 @@ async def on_GET(self, request):
guests = parse_boolean(request, "guests", default=True)
deactivated = parse_boolean(request, "deactivated", default=False)

users = await self.store.get_users_paginate(
users, total = await self.store.get_users_paginate(
start, limit, user_id, guests, deactivated
)
ret = {"users": users}
ret = {"users": users, "total": total}
if len(users) >= limit:
ret["next_token"] = str(start + len(users))

Expand Down Expand Up @@ -196,7 +196,7 @@ async def on_PUT(self, request, user_id):
user_id, threepid["medium"], threepid["address"], current_time
)

if "avatar_url" in body:
if "avatar_url" in body and type(body["avatar_url"]) == str:
await self.profile_handler.set_avatar_url(
target_user, requester, body["avatar_url"], True
)
Expand Down Expand Up @@ -271,7 +271,7 @@ async def on_PUT(self, request, user_id):
user_id, threepid["medium"], threepid["address"], current_time
)

if "avatar_url" in body:
if "avatar_url" in body and type(body["avatar_url"]) == str:
await self.profile_handler.set_avatar_url(
user_id, requester, body["avatar_url"], True
)
Expand Down
68 changes: 39 additions & 29 deletions synapse/storage/data_stores/main/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,8 @@ def get_users_paginate(
self, start, limit, name=None, guests=True, deactivated=False
):
"""Function to retrieve a paginated list of users from
users list. This will return a json list of users.
users list. This will return a json list of users and the
total number of users matching the filter criteria.

Args:
start (int): start number to begin the query from
Expand All @@ -518,35 +519,44 @@ def get_users_paginate(
guests (bool): whether to in include guest users
deactivated (bool): whether to include deactivated users
Returns:
defer.Deferred: resolves to list[dict[str, Any]]
defer.Deferred: resolves to list[dict[str, Any]], int
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
"""
name_filter = {}
if name:
name_filter["name"] = "%" + name + "%"

attr_filter = {}
if not guests:
attr_filter["is_guest"] = 0
if not deactivated:
attr_filter["deactivated"] = 0

return self.db.simple_select_list_paginate(
desc="get_users_paginate",
table="users",
orderby="name",
start=start,
limit=limit,
filters=name_filter,
keyvalues=attr_filter,
retcols=[
"name",
"password_hash",
"is_guest",
"admin",
"user_type",
"deactivated",
],
)

def get_users_paginate_txn(txn):
filters = []
args = []

if name:
filters.append("name LIKE ?")
args.append("%" + name + "%")

if not guests:
filters.append("is_guest = 0")

if not deactivated:
filters.append("deactivated = 0")

where_clause = "WHERE " + " AND ".join(filters) if len(filters) > 0 else ""

sql = "SELECT COUNT(*) as total_users FROM users %s" % (where_clause)
txn.execute(sql, args)
count = txn.fetchone()[0]

args = [self.hs.config.server_name] + args + [limit, start]
sql = """
SELECT name, user_type, is_guest, admin, deactivated, displayname, avatar_url
FROM users as u
LEFT JOIN profiles AS p ON u.name = '@' || p.user_id || ':' || ?
{}
ORDER BY u.name LIMIT ? OFFSET ?
""".format(
where_clause
)
txn.execute(sql, args)
users = self.db.cursor_to_dict(txn)
return users, count

return self.db.runInteraction("get_users_paginate_txn", get_users_paginate_txn)

def search_users(self, term):
"""Function to search users list for one or more users with
Expand Down
2 changes: 2 additions & 0 deletions tests/rest/admin/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ def test_all_users(self):

self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
self.assertEqual(3, len(channel.json_body["users"]))
self.assertEqual(3, channel.json_body["total"])


class UserRestTestCase(unittest.HomeserverTestCase):
Expand Down Expand Up @@ -412,6 +413,7 @@ def test_requester_is_admin(self):
"password": "abc123",
"admin": True,
"threepids": [{"medium": "email", "address": "bob@bob.bob"}],
"avatar_url": None,
}
)

Expand Down
46 changes: 46 additions & 0 deletions tests/storage/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
awesome-manuel marked this conversation as resolved.
Show resolved Hide resolved
# Copyright 2020 Awesome Technologies Innovationslabor GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from twisted.internet import defer

from synapse.types import UserID

from tests import unittest
from tests.utils import setup_test_homeserver


class DataStoreTestCase(unittest.TestCase):
@defer.inlineCallbacks
def setUp(self):
hs = yield setup_test_homeserver(self.addCleanup)

self.store = hs.get_datastore()

self.user = UserID.from_string("@abcde:test")
self.displayname = "Frank"

@defer.inlineCallbacks
def test_get_users_paginate(self):
yield self.store.register_user(self.user.to_string(), "pass")
yield self.store.create_profile(self.user.localpart)
yield self.store.set_profile_displayname(self.user.localpart, self.displayname)

users, total = yield self.store.get_users_paginate(
0, 10, name="bc", guests=False
)

self.assertEquals(1, total)
self.assertEquals(self.displayname, users.pop()["displayname"])