channel-folders: Add PATCH method to reorder channel folders.

The test cases are copied from ReorderCustomProfileFieldTest since we
are imitating the reordering mechanism from custom profile fields to
channel folders.
This commit is contained in:
Shubham Padia
2025-08-06 08:04:39 +00:00
committed by Tim Abbott
parent 40132e200b
commit 22b231ab6f
8 changed files with 174 additions and 2 deletions

View File

@@ -74,6 +74,7 @@
* [Remove a default channel](/api/remove-default-stream)
* [Create a channel folder](/api/create-channel-folder)
* [Get channel folders](/api/get-channel-folders)
* [Reorder channel folders](/api/patch-channel-folders)
* [Update a channel folder](/api/update-channel-folder)
#### Users

View File

@@ -4,3 +4,6 @@
Added a new field `order` to show in which order should channel folders be
displayed. The list is 0-indexed and works similar to the `order` field of
custom profile fields.
* [`PATCH /channel_folders`](/api/patch-channel-folders): Added a new
endpoint for reordering channel folders. It accepts an array of channel
folder IDs arranged in the order the user desires it to be in.

View File

@@ -1,7 +1,11 @@
from collections.abc import Iterable
from django.db import transaction
from django.utils.timezone import now as timezone_now
from django.utils.translation import gettext as _
from zerver.lib.channel_folders import get_channel_folder_dict, render_channel_folder_description
from zerver.lib.exceptions import JsonableError
from zerver.models import ChannelFolder, Realm, RealmAuditLog, UserProfile
from zerver.models.realm_audit_logs import AuditLogEventType
from zerver.models.users import active_user_ids
@@ -22,6 +26,8 @@ def check_add_channel_folder(
rendered_description=rendered_description,
creator_id=acting_user.id,
)
channel_folder.order = channel_folder.id
channel_folder.save(update_fields=["order"])
creation_time = timezone_now()
RealmAuditLog.objects.create(
@@ -42,6 +48,18 @@ def check_add_channel_folder(
return channel_folder
@transaction.atomic(durable=True)
def try_reorder_realm_channel_folders(realm: Realm, order: Iterable[int]) -> None:
order_mapping = {_[1]: _[0] for _ in enumerate(order)}
channel_folders = ChannelFolder.objects.filter(realm=realm)
for channel_folder in channel_folders:
if channel_folder.id not in order_mapping:
raise JsonableError(_("Invalid order mapping."))
for channel_folder in channel_folders:
channel_folder.order = order_mapping[channel_folder.id]
channel_folder.save(update_fields=["order"])
def do_send_channel_folder_update_event(
channel_folder: ChannelFolder, data: dict[str, str | bool]
) -> None:

View File

@@ -11,6 +11,7 @@ from typing import Any
from django.utils.timezone import now as timezone_now
from zerver.actions.channel_folders import check_add_channel_folder
from zerver.actions.create_user import do_create_user
from zerver.actions.presence import update_user_presence
from zerver.actions.reactions import do_add_reaction
@@ -21,6 +22,7 @@ from zerver.lib.initial_password import initial_password
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.upload import upload_message_attachment
from zerver.models import Client, Message, NamedUserGroup, UserPresence
from zerver.models.channel_folders import ChannelFolder
from zerver.models.realms import get_realm
from zerver.models.users import UserProfile, get_user
from zerver.openapi.openapi import Parameter
@@ -389,3 +391,25 @@ def remove_attachment() -> dict[str, object]:
attachment_id = url.replace("/user_uploads/", "").split("/")[0]
return {"attachment_id": attachment_id}
@openapi_param_value_generator(["/channel_folders:patch"])
def add_channel_folders() -> dict[str, object]:
user_profile = helpers.example_user("iago")
realm = user_profile.realm
check_add_channel_folder(
realm,
"General",
"Channel for general discussions",
acting_user=user_profile,
)
check_add_channel_folder(
realm,
"Documentation",
"Channels for **documentation** discussions",
acting_user=user_profile,
)
check_add_channel_folder(realm, "Memes", "Channels for sharing memes", acting_user=user_profile)
channel_folders = ChannelFolder.objects.filter(realm=realm)
return {"order": [folder.id for folder in channel_folders]}

View File

@@ -24516,6 +24516,52 @@ paths:
},
],
}
patch:
operationId: patch-channel-folders
summary: Reorder channel folders
tags: ["channels"]
description: |
Given an array of channel folder IDs, this method will set the `order`
property of all of the channel folders in the organization according to
the order of the channel folder IDs specified in the request.
**Changes**: New in Zulip 11.0 (feature level ZF-fcae8c).
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
order:
type: array
description: |
A list of channel folder IDs representing the new order.
items:
type: integer
encoding:
order:
contentType: application/json
responses:
"200":
$ref: "#/components/responses/SimpleSuccess"
"400":
description: Bad request.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/CodedError"
- example:
{
"code": "BAD_REQUEST",
"msg": "Invalid order mapping",
"result": "error",
}
description: |
An example JSON response when the order mapping is invalid:
/channel_folders/{channel_folder_id}:
patch:
operationId: update-channel-folder

View File

@@ -21,7 +21,10 @@ class ChannelFoldersTestCase(ZulipTestCase):
lear_user = self.lear_user("cordelia")
check_add_channel_folder(
zulip_realm, "Frontend", "Channels for frontend discussions", acting_user=iago
zulip_realm,
"Frontend",
"Channels for frontend discussions",
acting_user=iago,
)
check_add_channel_folder(
zulip_realm, "Backend", "Channels for **backend** discussions", acting_user=iago
@@ -50,6 +53,7 @@ class ChannelFolderCreationTest(ZulipTestCase):
assert channel_folder is not None
self.assertEqual(channel_folder.name, "Frontend")
self.assertEqual(channel_folder.description, "")
self.assertEqual(channel_folder.id, channel_folder.order)
response = orjson.loads(result.content)
self.assertEqual(response["channel_folder_id"], channel_folder.id)
@@ -349,3 +353,65 @@ class UpdateChannelFoldersTest(ChannelFoldersTestCase):
self.assert_json_success(result)
channel_folder = ChannelFolder.objects.get(id=channel_folder_id)
self.assertTrue(channel_folder.is_archived)
class ReorderChannelFolderTest(ChannelFoldersTestCase):
def test_reorder(self) -> None:
self.login("iago")
realm = get_realm("zulip")
order = list(
ChannelFolder.objects.filter(realm=realm)
.order_by("-order")
.values_list("order", flat=True)
)
result = self.client_patch(
"/json/channel_folders", info={"order": orjson.dumps(order).decode()}
)
self.assert_json_success(result)
fields = ChannelFolder.objects.filter(realm=realm).order_by("order")
for field in fields:
self.assertEqual(field.id, order[field.order])
def test_reorder_duplicates(self) -> None:
self.login("iago")
realm = get_realm("zulip")
order = list(
ChannelFolder.objects.filter(realm=realm)
.order_by("-order")
.values_list("order", flat=True)
)
frontend_folder = ChannelFolder.objects.get(name="Frontend", realm=realm)
order.append(frontend_folder.id)
result = self.client_patch(
"/json/channel_folders", info={"order": orjson.dumps(order).decode()}
)
self.assert_json_success(result)
fields = ChannelFolder.objects.filter(realm=realm).order_by("order")
for field in fields:
self.assertEqual(field.id, order[field.order])
def test_reorder_unauthorized(self) -> None:
self.login("hamlet")
realm = get_realm("zulip")
order = list(
ChannelFolder.objects.filter(realm=realm)
.order_by("-order")
.values_list("order", flat=True)
)
result = self.client_patch(
"/json/channel_folders", info={"order": orjson.dumps(order).decode()}
)
self.assert_json_error(result, "Must be an organization administrator")
def test_reorder_invalid(self) -> None:
self.login("iago")
order = [100, 200, 300]
result = self.client_patch(
"/json/channel_folders", info={"order": orjson.dumps(order).decode()}
)
self.assert_json_error(result, "Invalid order mapping.")
order = [1, 2]
result = self.client_patch(
"/json/channel_folders", info={"order": orjson.dumps(order).decode()}
)
self.assert_json_error(result, "Invalid order mapping.")

View File

@@ -10,6 +10,7 @@ from zerver.actions.channel_folders import (
do_change_channel_folder_description,
do_change_channel_folder_name,
do_unarchive_channel_folder,
try_reorder_realm_channel_folders,
)
from zerver.decorator import require_realm_admin
from zerver.lib.channel_folders import (
@@ -52,6 +53,18 @@ def get_channel_folders(
return json_success(request, data={"channel_folders": channel_folders})
@require_realm_admin
@typed_endpoint
def reorder_realm_channel_folders(
request: HttpRequest,
user_profile: UserProfile,
*,
order: Json[list[int]],
) -> HttpResponse:
try_reorder_realm_channel_folders(user_profile.realm, order)
return json_success(request)
@require_realm_admin
@typed_endpoint
def update_channel_folder(

View File

@@ -48,6 +48,7 @@ from zerver.views.auth import (
from zerver.views.channel_folders import (
create_channel_folder,
get_channel_folders,
reorder_realm_channel_folders,
update_channel_folder,
)
from zerver.views.compatibility import check_global_compatibility
@@ -559,7 +560,7 @@ v1_api_and_json_patterns = [
DELETE=remove_subscriptions_backend,
),
rest_path("channel_folders/create", POST=create_channel_folder),
rest_path("channel_folders", GET=get_channel_folders),
rest_path("channel_folders", GET=get_channel_folders, PATCH=reorder_realm_channel_folders),
rest_path("channel_folders/<int:channel_folder_id>", PATCH=update_channel_folder),
# topic-muting -> zerver.views.user_topics
# (deprecated and will be removed once clients are migrated to use '/user_topics')