api: Support user_id in get_user_presence_endpoint.

This is part of our general process of replacing emails, which are not
static with time, with user_ids when referring to users in the API.

We still keep the `email` reference option, since it can be useful for
linking third-party applications to Zulip on an intranet that might
have a user's corporate email handy and not want to do the extra round
trip to lookup the user.

The name of the parameter, user_id_or_email, was chosen to to make it
clear that the default/preferred option is user_id.

Fixes #14304.
This commit is contained in:
Tushar912 2021-01-05 00:06:00 +05:30 committed by Tim Abbott
parent dc67870e0c
commit 55de66f944
10 changed files with 64 additions and 20 deletions

View File

@ -10,6 +10,11 @@ below features are supported.
## Changes in Zulip 4.0 ## Changes in Zulip 4.0
**Feature level 43**
* [`GET /users/{user_id_or_email}/presence`]: Added support for
passing the `user_id` to identify the target user.
**Feature level 42** **Feature level 42**
* `PATCH /settings/display`: Added a new `default_view` setting allowing * `PATCH /settings/display`: Added a new `default_view` setting allowing

View File

@ -1,32 +1,32 @@
# Get user presence # Get user presence
{generate_api_description(/users/{email}/presence:get)} {generate_api_description(/users/{user_id_or_email}/presence:get)}
## Usage examples ## Usage examples
{start_tabs} {start_tabs}
{tab|python} {tab|python}
{generate_code_example(python)|/users/{email}/presence:get|example} {generate_code_example(python)|/users/{user_id_or_email}/presence:get|example}
{tab|curl} {tab|curl}
{generate_code_example(curl)|/users/{email}/presence:get|example} {generate_code_example(curl)|/users/{user_id_or_email}/presence:get|example}
{end_tabs} {end_tabs}
## Parameters ## Parameters
{generate_api_arguments_table|zulip.yaml|/users/{email}/presence:get} {generate_api_arguments_table|zulip.yaml|/users/{user_id_or_email}/presence:get}
## Response ## Response
#### Return values #### Return values
{generate_return_values_table|zulip.yaml|/users/{email}/presence:get} {generate_return_values_table|zulip.yaml|/users/{user_id_or_email}/presence:get}
#### Example response #### Example response
A typical successful JSON response may look like: A typical successful JSON response may look like:
{generate_code_example|/users/{email}/presence:get|fixture(200)} {generate_code_example|/users/{user_id_or_email}/presence:get|fixture(200)}

View File

@ -30,7 +30,7 @@ DESKTOP_WARNING_VERSION = "5.2.0"
# #
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# new level means in templates/zerver/api/changelog.md. # new level means in templates/zerver/api/changelog.md.
API_FEATURE_LEVEL = 42 API_FEATURE_LEVEL = 43
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump # only when going from an old version of the code to a newer version. Bump

View File

@ -219,7 +219,7 @@ def delete_event_queue() -> Dict[str, object]:
} }
@openapi_param_value_generator(["/users/{email}/presence:get"]) @openapi_param_value_generator(["/users/{user_id_or_email}/presence:get"])
def get_user_presence() -> Dict[str, object]: def get_user_presence() -> Dict[str, object]:
iago = helpers.example_user("iago") iago = helpers.example_user("iago")
client = Client.objects.create(name="curl-test-client-3") client = Client.objects.create(name="curl-test-client-3")

View File

@ -140,7 +140,7 @@ def test_authorization_errors_fatal(client: Client, nonadmin_client: Client) ->
validate_against_openapi_schema(result, "/users/me/subscriptions", "post", "400_1") validate_against_openapi_schema(result, "/users/me/subscriptions", "post", "400_1")
@openapi_test_function("/users/{email}/presence:get") @openapi_test_function("/users/{user_id_or_email}/presence:get")
def get_user_presence(client: Client) -> None: def get_user_presence(client: Client) -> None:
# {code_example|start} # {code_example|start}
@ -148,7 +148,7 @@ def get_user_presence(client: Client) -> None:
result = client.get_user_presence("iago@zulip.com") result = client.get_user_presence("iago@zulip.com")
# {code_example|end} # {code_example|end}
validate_against_openapi_schema(result, "/users/{email}/presence", "get", "200") validate_against_openapi_schema(result, "/users/{user_id_or_email}/presence", "get", "200")
@openapi_test_function("/users/me/presence:post") @openapi_test_function("/users/me/presence:post")

View File

@ -4260,7 +4260,7 @@ paths:
- $ref: "#/components/schemas/JsonSuccess" - $ref: "#/components/schemas/JsonSuccess"
- example: {"msg": "", "result": "success"} - example: {"msg": "", "result": "success"}
/users/{email}/presence: /users/{user_id_or_email}/presence:
get: get:
operationId: get_user_presence operationId: get_user_presence
tags: ["users"] tags: ["users"]
@ -4273,16 +4273,19 @@ paths:
presence endpoint, which returns data for all active users in the presence endpoint, which returns data for all active users in the
organization, instead. organization, instead.
`GET {{ api_url }}/v1/users/{email}/presence` `GET {{ api_url }}/v1/users/{user_id_or_email}/presence`
See See
[Zulip's developer documentation](https://zulip.readthedocs.io/en/latest/subsystems/presence.html) [Zulip's developer documentation](https://zulip.readthedocs.io/en/latest/subsystems/presence.html)
for details on the data model for presence in Zulip. for details on the data model for presence in Zulip.
parameters: parameters:
- name: email - name: user_id_or_email
in: path in: path
description: | description: |
The email address of the user whose presence you want to fetch. The user_id or Zulip display email address of the user whose presence you want to fetch.
**Changes**: New in Zulip 4.0 (feature level 43). Previous versions only supported
identifying the user by Zulip display email.
schema: schema:
type: string type: string
example: iago@zulip.com example: iago@zulip.com

View File

@ -1063,7 +1063,10 @@ class OpenAPIRegexTest(ZulipTestCase):
find_openapi_endpoint("/users/23/subscriptions/21") find_openapi_endpoint("/users/23/subscriptions/21")
== "/users/{user_id}/subscriptions/{stream_id}" == "/users/{user_id}/subscriptions/{stream_id}"
) )
assert find_openapi_endpoint("/users/iago@zulip.com/presence") == "/users/{email}/presence" assert (
find_openapi_endpoint("/users/iago@zulip.com/presence")
== "/users/{user_id_or_email}/presence"
)
assert find_openapi_endpoint("/users/iago@zulip.com") == "/users/{email}" assert find_openapi_endpoint("/users/iago@zulip.com") == "/users/{email}"
assert find_openapi_endpoint("/messages/23") == "/messages/{message_id}" assert find_openapi_endpoint("/messages/23") == "/messages/{message_id}"
assert find_openapi_endpoint("/realm/emoji/realm_emoji_1") == "/realm/emoji/{emoji_name}" assert find_openapi_endpoint("/realm/emoji/realm_emoji_1") == "/realm/emoji/{emoji_name}"

View File

@ -464,10 +464,17 @@ class SingleUserPresenceTests(ZulipTestCase):
result = self.client_get("/json/users/cordelia@zulip.com/presence") result = self.client_get("/json/users/cordelia@zulip.com/presence")
self.assert_json_error(result, "No presence data for cordelia@zulip.com") self.assert_json_error(result, "No presence data for cordelia@zulip.com")
cordelia = self.example_user("cordelia")
result = self.client_get(f"/json/users/{cordelia.id}/presence")
self.assert_json_error(result, f"No presence data for {cordelia.id}")
do_deactivate_user(self.example_user("cordelia")) do_deactivate_user(self.example_user("cordelia"))
result = self.client_get("/json/users/cordelia@zulip.com/presence") result = self.client_get("/json/users/cordelia@zulip.com/presence")
self.assert_json_error(result, "No such user") self.assert_json_error(result, "No such user")
result = self.client_get(f"/json/users/{cordelia.id}/presence")
self.assert_json_error(result, "No such user")
result = self.client_get("/json/users/default-bot@zulip.com/presence") result = self.client_get("/json/users/default-bot@zulip.com/presence")
self.assert_json_error(result, "Presence is not supported for bot users.") self.assert_json_error(result, "Presence is not supported for bot users.")
@ -476,6 +483,10 @@ class SingleUserPresenceTests(ZulipTestCase):
result = self.client_get("/json/users/othello@zulip.com/presence", subdomain="zephyr") result = self.client_get("/json/users/othello@zulip.com/presence", subdomain="zephyr")
self.assert_json_error(result, "No such user") self.assert_json_error(result, "No such user")
othello = self.example_user("othello")
result = self.client_get(f"/json/users/{othello.id}/presence", subdomain="zephyr")
self.assert_json_error(result, "No such user")
# Then, we check everything works # Then, we check everything works
self.login("hamlet") self.login("hamlet")
result = self.client_get("/json/users/othello@zulip.com/presence") result = self.client_get("/json/users/othello@zulip.com/presence")
@ -485,6 +496,13 @@ class SingleUserPresenceTests(ZulipTestCase):
) )
self.assertEqual(set(result_dict["presence"]["website"].keys()), {"status", "timestamp"}) self.assertEqual(set(result_dict["presence"]["website"].keys()), {"status", "timestamp"})
result = self.client_get(f"/json/users/{othello.id}/presence")
result_dict = result.json()
self.assertEqual(
set(result_dict["presence"].keys()), {"ZulipAndroid", "website", "aggregated"}
)
self.assertEqual(set(result_dict["presence"]["website"].keys()), {"status", "timestamp"})
def test_ping_only(self) -> None: def test_ping_only(self) -> None:
self.login("othello") self.login("othello")

View File

@ -13,25 +13,40 @@ from zerver.lib.request import REQ, JsonableError, has_request_variables
from zerver.lib.response import json_error, json_success from zerver.lib.response import json_error, json_success
from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.timestamp import datetime_to_timestamp
from zerver.lib.validator import check_bool, check_capped_string from zerver.lib.validator import check_bool, check_capped_string
from zerver.models import UserActivity, UserPresence, UserProfile, get_active_user from zerver.models import (
UserActivity,
UserPresence,
UserProfile,
get_active_user,
get_active_user_profile_by_id_in_realm,
)
def get_presence_backend( def get_presence_backend(
request: HttpRequest, user_profile: UserProfile, email: str request: HttpRequest, user_profile: UserProfile, user_id_or_email: str
) -> HttpResponse: ) -> HttpResponse:
# This isn't used by the webapp; it's available for API use by # This isn't used by the webapp; it's available for API use by
# bots and other clients. We may want to add slim_presence # bots and other clients. We may want to add slim_presence
# support for it (or just migrate its API wholesale) later. # support for it (or just migrate its API wholesale) later.
try: try:
target = get_active_user(email, user_profile.realm) try:
user_id = int(user_id_or_email)
target = get_active_user_profile_by_id_in_realm(user_id, user_profile.realm)
except ValueError:
email = user_id_or_email
target = get_active_user(email, user_profile.realm)
except UserProfile.DoesNotExist: except UserProfile.DoesNotExist:
return json_error(_("No such user")) return json_error(_("No such user"))
if target.is_bot: if target.is_bot:
return json_error(_("Presence is not supported for bot users.")) return json_error(_("Presence is not supported for bot users."))
presence_dict = get_presence_for_user(target.id) presence_dict = get_presence_for_user(target.id)
if len(presence_dict) == 0: if len(presence_dict) == 0:
return json_error(_("No presence data for {email}").format(email=email)) return json_error(
_("No presence data for {user_id_or_email}").format(user_id_or_email=user_id_or_email)
)
# For initial version, we just include the status and timestamp keys # For initial version, we just include the status and timestamp keys
result = dict(presence=presence_dict[target.email]) result = dict(presence=presence_dict[target.email])

View File

@ -371,7 +371,7 @@ v1_api_and_json_patterns = [
# It's important that this sit after users/me/presence so that # It's important that this sit after users/me/presence so that
# Django's URL resolution order doesn't break the # Django's URL resolution order doesn't break the
# /users/me/presence endpoint. # /users/me/presence endpoint.
rest_path("users/<email>/presence", GET=get_presence_backend), rest_path("users/<user_id_or_email>/presence", GET=get_presence_backend),
rest_path("realm/presence", GET=get_statuses_for_realm), rest_path("realm/presence", GET=get_statuses_for_realm),
rest_path("users/me/status", POST=update_user_status_backend), rest_path("users/me/status", POST=update_user_status_backend),
# user_groups -> zerver.views.user_groups # user_groups -> zerver.views.user_groups