mirror of https://github.com/zulip/zulip.git
users: Add API endpoint to update_user_backend by real email.
The old endpoint for updating a user worked only via user id. Now we add a different entry to this functionality, fetching the user by .delivery_email. update_user_backend becomes the main function handling all the logic, invoked by the two endpoints.
This commit is contained in:
parent
389b851f81
commit
77e7a2d30f
|
@ -66,6 +66,7 @@
|
|||
* [Get all users](/api/get-users)
|
||||
* [Create a user](/api/create-user)
|
||||
* [Update a user](/api/update-user)
|
||||
* [Update a user by email](/api/update-user-by-email)
|
||||
* [Deactivate a user](/api/deactivate-user)
|
||||
* [Deactivate own user](/api/deactivate-own-user)
|
||||
* [Reactivate a user](/api/reactivate-user)
|
||||
|
|
|
@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
|
|||
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
||||
# entries in the endpoint's documentation in `zulip.yaml`.
|
||||
|
||||
API_FEATURE_LEVEL = 313 # Last bumped for adding `new_email` to /users/{user_id}
|
||||
API_FEATURE_LEVEL = 313 # Last bumped for adding `new_email` to /users/{user_id} and the new PATCH /users/{email} endpoint
|
||||
|
||||
# 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
|
||||
|
|
|
@ -23,7 +23,7 @@ from zerver.lib.upload import upload_message_attachment
|
|||
from zerver.lib.users import get_api_key
|
||||
from zerver.models import Client, Message, NamedUserGroup, UserPresence
|
||||
from zerver.models.realms import get_realm
|
||||
from zerver.models.users import get_user
|
||||
from zerver.models.users import UserProfile, get_user
|
||||
from zerver.openapi.openapi import Parameter
|
||||
|
||||
GENERATOR_FUNCTIONS: dict[str, Callable[[], dict[str, object]]] = {}
|
||||
|
@ -252,6 +252,19 @@ def create_user() -> dict[str, object]:
|
|||
}
|
||||
|
||||
|
||||
@openapi_param_value_generator(["/users/{email]:patch", "/users/{user_id}:patch"])
|
||||
def new_email_value() -> dict[str, object]:
|
||||
count = 0
|
||||
exists = True
|
||||
while exists:
|
||||
email = f"new{count}@zulip.com"
|
||||
exists = UserProfile.objects.filter(delivery_email=email).exists()
|
||||
count += 1
|
||||
return {
|
||||
"new_email": email,
|
||||
}
|
||||
|
||||
|
||||
@openapi_param_value_generator(["/user_groups/create:post"])
|
||||
def create_user_group_data() -> dict[str, object]:
|
||||
return {
|
||||
|
|
|
@ -12634,6 +12634,52 @@ paths:
|
|||
"delivery_email": null,
|
||||
},
|
||||
}
|
||||
patch:
|
||||
operationId: update-user-by-email
|
||||
summary: Update a user by email
|
||||
tags: ["users"]
|
||||
x-requires-administrator: true
|
||||
description: |
|
||||
Administrative endpoint to update the details of another user in the organization by their email address.
|
||||
Works the same way as [`PATCH /users/{user_id}`](/api/update-user) but fetching the target user by their
|
||||
real email address.
|
||||
|
||||
The requester needs to have permission to view the target user's real email address, subject to the
|
||||
user's email address visibility setting. Otherwise, the dummy address of the format
|
||||
`user{id}@{realm.host}` needs be used. This follows the same rules as `GET /users/{email}`.
|
||||
|
||||
**Changes**: New in Zulip 10.0 (feature level 313).
|
||||
parameters:
|
||||
- name: email
|
||||
in: path
|
||||
required: true
|
||||
description: |
|
||||
The email address of the user, specified following the same rules as
|
||||
[`GET /users/{email}`](/api/get-user-by-email).
|
||||
schema:
|
||||
type: string
|
||||
example: hamlet@zulip.com
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/UpdateUser"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/SimpleSuccess"
|
||||
|
||||
"400":
|
||||
description: Bad request.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/CodedError"
|
||||
- example:
|
||||
{
|
||||
"result": "error",
|
||||
"msg": "Guests cannot be organization administrators",
|
||||
"code": "BAD_REQUEST",
|
||||
}
|
||||
description: |
|
||||
A typical unsuccessful JSON response:
|
||||
/users/{user_id}:
|
||||
get:
|
||||
operationId: get-user
|
||||
|
@ -12738,62 +12784,7 @@ paths:
|
|||
parameters:
|
||||
- $ref: "#/components/parameters/UserId"
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
full_name:
|
||||
description: |
|
||||
The user's full name.
|
||||
|
||||
**Changes**: Removed unnecessary JSON-encoding of this parameter in
|
||||
Zulip 5.0 (feature level 106).
|
||||
type: string
|
||||
example: NewName
|
||||
role:
|
||||
description: |
|
||||
New [role](/api/roles-and-permissions) for the user. Roles are encoded as:
|
||||
|
||||
- Organization owner: 100
|
||||
- Organization administrator: 200
|
||||
- Organization moderator: 300
|
||||
- Member: 400
|
||||
- Guest: 600
|
||||
|
||||
Only organization owners can add or remove the owner role.
|
||||
|
||||
The owner role cannot be removed from the only organization owner.
|
||||
|
||||
**Changes**: New in Zulip 3.0 (feature level 8), replacing the previous
|
||||
pair of `is_admin` and `is_guest` boolean parameters. Organization moderator
|
||||
role added in Zulip 4.0 (feature level 60).
|
||||
type: integer
|
||||
example: 400
|
||||
profile_data:
|
||||
description: |
|
||||
A dictionary containing the to be updated custom profile field data for the user.
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
example:
|
||||
[{"id": 4, "value": "0"}, {"id": 5, "value": "1909-04-05"}]
|
||||
new_email:
|
||||
description: |
|
||||
New email address for the user. Requires the user making
|
||||
the request to be an organization administrator and
|
||||
additionally have the `.can_change_user_emails` special
|
||||
permission.
|
||||
|
||||
**Changes**: New in Zulip 10.0 (feature level 313).
|
||||
type: string
|
||||
example: username@example.com
|
||||
encoding:
|
||||
role:
|
||||
contentType: application/json
|
||||
profile_data:
|
||||
contentType: application/json
|
||||
$ref: "#/components/requestBodies/UpdateUser"
|
||||
|
||||
responses:
|
||||
"200":
|
||||
|
@ -24475,6 +24466,67 @@ components:
|
|||
allOf:
|
||||
- $ref: "#/components/schemas/IgnoredParametersSuccess"
|
||||
|
||||
################
|
||||
# Shared request bodies
|
||||
################
|
||||
requestBodies:
|
||||
UpdateUser:
|
||||
required: false
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
full_name:
|
||||
description: |
|
||||
The user's full name.
|
||||
|
||||
**Changes**: Removed unnecessary JSON-encoding of this parameter in
|
||||
Zulip 5.0 (feature level 106).
|
||||
type: string
|
||||
example: NewName
|
||||
role:
|
||||
description: |
|
||||
New [role](/api/roles-and-permissions) for the user. Roles are encoded as:
|
||||
|
||||
- Organization owner: 100
|
||||
- Organization administrator: 200
|
||||
- Organization moderator: 300
|
||||
- Member: 400
|
||||
- Guest: 600
|
||||
|
||||
Only organization owners can add or remove the owner role.
|
||||
|
||||
The owner role cannot be removed from the only organization owner.
|
||||
|
||||
**Changes**: New in Zulip 3.0 (feature level 8), replacing the previous
|
||||
pair of `is_admin` and `is_guest` boolean parameters. Organization moderator
|
||||
role added in Zulip 4.0 (feature level 60).
|
||||
type: integer
|
||||
example: 400
|
||||
profile_data:
|
||||
description: |
|
||||
A dictionary containing the updated custom profile field data for the user.
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
example:
|
||||
[{"id": 4, "value": "0"}, {"id": 5, "value": "1909-04-05"}]
|
||||
new_email:
|
||||
description: |
|
||||
New email address for the user. Requires the user making the request
|
||||
to be an organization owner and additionally have the `.can_change_user_emails`
|
||||
special permission.
|
||||
|
||||
**Changes**: New in Zulip 10.0 (feature level 285).
|
||||
type: string
|
||||
example: username@example.com
|
||||
encoding:
|
||||
role:
|
||||
contentType: application/json
|
||||
profile_data:
|
||||
contentType: application/json
|
||||
|
||||
####################
|
||||
# Shared parameters
|
||||
####################
|
||||
|
|
|
@ -1121,6 +1121,63 @@ class BulkCreateUserTest(ZulipTestCase):
|
|||
)
|
||||
|
||||
|
||||
class UpdateUserByEmailEndpointTest(ZulipTestCase):
|
||||
def test_update_user_by_email(self) -> None:
|
||||
self.login("iago")
|
||||
hamlet = self.example_user("hamlet")
|
||||
do_change_user_setting(
|
||||
hamlet,
|
||||
"email_address_visibility",
|
||||
UserProfile.EMAIL_ADDRESS_VISIBILITY_EVERYONE,
|
||||
acting_user=None,
|
||||
)
|
||||
|
||||
result = self.client_patch(
|
||||
f"/json/users/{hamlet.delivery_email}",
|
||||
dict(full_name="Newname"),
|
||||
)
|
||||
self.assert_json_success(result)
|
||||
hamlet.refresh_from_db()
|
||||
self.assertEqual(hamlet.full_name, "Newname")
|
||||
|
||||
do_change_user_setting(
|
||||
hamlet,
|
||||
"email_address_visibility",
|
||||
UserProfile.EMAIL_ADDRESS_VISIBILITY_MEMBERS,
|
||||
acting_user=None,
|
||||
)
|
||||
result = self.client_patch(
|
||||
f"/json/users/{hamlet.delivery_email}",
|
||||
dict(full_name="Newname2"),
|
||||
)
|
||||
self.assert_json_success(result)
|
||||
hamlet.refresh_from_db()
|
||||
self.assertEqual(hamlet.full_name, "Newname2")
|
||||
|
||||
do_change_user_setting(
|
||||
hamlet,
|
||||
"email_address_visibility",
|
||||
UserProfile.EMAIL_ADDRESS_VISIBILITY_NOBODY,
|
||||
acting_user=None,
|
||||
)
|
||||
result = self.client_patch(
|
||||
f"/json/users/{hamlet.delivery_email}",
|
||||
dict(full_name="Newname2"),
|
||||
)
|
||||
self.assert_json_error(result, "No such user")
|
||||
|
||||
# The dummy email can be used, when we don't have access
|
||||
# to the target's email address.
|
||||
dummy_email = hamlet.email
|
||||
result = self.client_patch(
|
||||
f"/json/users/{dummy_email}",
|
||||
dict(full_name="Newname3"),
|
||||
)
|
||||
self.assert_json_success(result)
|
||||
hamlet.refresh_from_db()
|
||||
self.assertEqual(hamlet.full_name, "Newname3")
|
||||
|
||||
|
||||
class AdminChangeUserEmailTest(ZulipTestCase):
|
||||
def test_change_user_email_backend(self) -> None:
|
||||
cordelia = self.example_user("cordelia")
|
||||
|
|
|
@ -202,7 +202,7 @@ class ProfileDataElement(BaseModel):
|
|||
|
||||
@typed_endpoint
|
||||
@transaction.atomic(durable=True)
|
||||
def update_user_backend(
|
||||
def update_user_by_id_api(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
|
@ -215,7 +215,53 @@ def update_user_backend(
|
|||
target = access_user_by_id(
|
||||
user_profile, user_id, allow_deactivated=True, allow_bots=True, for_admin=True
|
||||
)
|
||||
return update_user_backend(
|
||||
request,
|
||||
user_profile,
|
||||
target,
|
||||
full_name=full_name,
|
||||
role=role,
|
||||
profile_data=profile_data,
|
||||
new_email=new_email,
|
||||
)
|
||||
|
||||
|
||||
@typed_endpoint
|
||||
@transaction.atomic(durable=True)
|
||||
def update_user_by_email_api(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
email: PathOnly[str],
|
||||
full_name: str | None = None,
|
||||
role: Json[RoleParamType] | None = None,
|
||||
profile_data: Json[list[ProfileDataElement]] | None = None,
|
||||
new_email: str | None = None,
|
||||
) -> HttpResponse:
|
||||
target = access_user_by_email(
|
||||
user_profile, email, allow_deactivated=True, allow_bots=True, for_admin=True
|
||||
)
|
||||
return update_user_backend(
|
||||
request,
|
||||
user_profile,
|
||||
target,
|
||||
full_name=full_name,
|
||||
role=role,
|
||||
profile_data=profile_data,
|
||||
new_email=new_email,
|
||||
)
|
||||
|
||||
|
||||
def update_user_backend(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
target: UserProfile,
|
||||
*,
|
||||
full_name: str | None = None,
|
||||
role: Json[RoleParamType] | None = None,
|
||||
profile_data: Json[list[ProfileDataElement]] | None = None,
|
||||
new_email: str | None = None,
|
||||
) -> HttpResponse:
|
||||
if new_email is not None and (
|
||||
not user_profile.can_change_user_emails or not user_profile.is_realm_admin
|
||||
):
|
||||
|
|
|
@ -232,7 +232,8 @@ from zerver.views.users import (
|
|||
patch_bot_backend,
|
||||
reactivate_user_backend,
|
||||
regenerate_bot_api_key,
|
||||
update_user_backend,
|
||||
update_user_by_email_api,
|
||||
update_user_by_id_api,
|
||||
)
|
||||
from zerver.views.video_calls import (
|
||||
complete_zoom_user,
|
||||
|
@ -316,11 +317,11 @@ v1_api_and_json_patterns = [
|
|||
rest_path(
|
||||
"users/<int:user_id>",
|
||||
GET=get_members_backend,
|
||||
PATCH=update_user_backend,
|
||||
PATCH=update_user_by_id_api,
|
||||
DELETE=deactivate_user_backend,
|
||||
),
|
||||
rest_path("users/<int:user_id>/subscriptions/<int:stream_id>", GET=get_subscription_backend),
|
||||
rest_path("users/<email>", GET=get_user_by_email),
|
||||
rest_path("users/<email>", GET=get_user_by_email, PATCH=update_user_by_email_api),
|
||||
rest_path("bots", GET=get_bots_backend, POST=add_bot_backend),
|
||||
rest_path("bots/<int:bot_id>/api_key/regenerate", POST=regenerate_bot_api_key),
|
||||
rest_path("bots/<int:bot_id>", PATCH=patch_bot_backend, DELETE=deactivate_bot_backend),
|
||||
|
|
Loading…
Reference in New Issue