channel_folder: Add API to create a channel folder.

This commit also includes code to include channel_folders
data in register response.

Fixes part of #31972.
This commit is contained in:
Sahil Batra
2025-05-02 19:20:22 +05:30
committed by Tim Abbott
parent 350f6a1fa1
commit 332abd9e91
17 changed files with 471 additions and 4 deletions

View File

@@ -63,6 +63,7 @@
* [Delete a topic](/api/delete-topic) * [Delete a topic](/api/delete-topic)
* [Add a default channel](/api/add-default-stream) * [Add a default channel](/api/add-default-stream)
* [Remove a default channel](/api/remove-default-stream) * [Remove a default channel](/api/remove-default-stream)
* [Create a channel folder](/api/create-channel-folder)
#### Users #### Users

View File

@@ -0,0 +1,8 @@
* [`POST /channel_folders/create`](/api/create-channel-folder): Added
a new endpoint for creating a new channel folder.
* [`GET /events`](/api/get-events): An event with `type: "channel_folder"` is
sent to all users when a channel folder is created.
* [`POST /register`](/api/register-queue): Added `channel_folders` field to
response.

View File

@@ -0,0 +1,43 @@
from django.db import transaction
from django.utils.timezone import now as timezone_now
from zerver.lib.channel_folders import get_channel_folder_dict, render_channel_folder_description
from zerver.models import ChannelFolder, RealmAuditLog, UserProfile
from zerver.models.realm_audit_logs import AuditLogEventType
from zerver.models.users import active_user_ids
from zerver.tornado.django_api import send_event_on_commit
@transaction.atomic(durable=True)
def check_add_channel_folder(
name: str, description: str, *, acting_user: UserProfile
) -> ChannelFolder:
realm = acting_user.realm
rendered_description = render_channel_folder_description(
description, realm, acting_user=acting_user
)
channel_folder = ChannelFolder.objects.create(
realm=realm,
name=name,
description=description,
rendered_description=rendered_description,
creator_id=acting_user.id,
)
creation_time = timezone_now()
RealmAuditLog.objects.create(
realm=realm,
acting_user=acting_user,
event_type=AuditLogEventType.CHANNEL_FOLDER_CREATED,
event_time=creation_time,
modified_channel_folder=channel_folder,
)
event = dict(
type="channel_folder",
op="add",
channel_folder=get_channel_folder_dict(channel_folder),
)
send_event_on_commit(realm, event, active_user_ids(realm.id))
return channel_folder

View File

@@ -0,0 +1,60 @@
from typing import TypedDict
from django.utils.translation import gettext as _
from zerver.lib.exceptions import JsonableError
from zerver.lib.markdown import markdown_convert
from zerver.lib.string_validation import check_string_is_printable
from zerver.lib.timestamp import datetime_to_timestamp
from zerver.models import ChannelFolder, Realm, UserProfile
class ChannelFolderDict(TypedDict):
id: int
name: str
description: str
rendered_description: str
creator_id: int | None
date_created: int
is_archived: bool
def check_channel_folder_name(name: str, realm: Realm) -> None:
if name.strip() == "":
raise JsonableError(_("Channel folder name can't be empty."))
invalid_character_pos = check_string_is_printable(name)
if invalid_character_pos is not None:
raise JsonableError(
_("Invalid character in channel folder name, at position {position}.").format(
position=invalid_character_pos
)
)
if ChannelFolder.objects.filter(name__iexact=name, realm=realm).exists():
raise JsonableError(_("Channel folder '{name}' already exists").format(name=name))
def render_channel_folder_description(text: str, realm: Realm, *, acting_user: UserProfile) -> str:
return markdown_convert(
text, message_realm=realm, no_previews=True, acting_user=acting_user
).rendered_content
def get_channel_folder_dict(channel_folder: ChannelFolder) -> ChannelFolderDict:
date_created = datetime_to_timestamp(channel_folder.date_created)
return ChannelFolderDict(
id=channel_folder.id,
name=channel_folder.name,
description=channel_folder.description,
rendered_description=channel_folder.rendered_description,
date_created=date_created,
creator_id=channel_folder.creator_id,
is_archived=channel_folder.is_archived,
)
def get_channel_folders_in_realm(realm: Realm) -> list[ChannelFolderDict]:
folders = ChannelFolder.objects.filter(realm=realm)
channel_folders = [get_channel_folder_dict(channel_folder) for channel_folder in folders]
return sorted(channel_folders, key=lambda folder: folder["id"])

View File

@@ -23,6 +23,7 @@ from zerver.lib.event_types import (
EventAttachmentAdd, EventAttachmentAdd,
EventAttachmentRemove, EventAttachmentRemove,
EventAttachmentUpdate, EventAttachmentUpdate,
EventChannelFolderAdd,
EventCustomProfileFields, EventCustomProfileFields,
EventDefaultStreamGroups, EventDefaultStreamGroups,
EventDefaultStreams, EventDefaultStreams,
@@ -161,6 +162,7 @@ check_alert_words = make_checker(EventAlertWords)
check_attachment_add = make_checker(EventAttachmentAdd) check_attachment_add = make_checker(EventAttachmentAdd)
check_attachment_remove = make_checker(EventAttachmentRemove) check_attachment_remove = make_checker(EventAttachmentRemove)
check_attachment_update = make_checker(EventAttachmentUpdate) check_attachment_update = make_checker(EventAttachmentUpdate)
check_channel_folder_add = make_checker(EventChannelFolderAdd)
check_custom_profile_fields = make_checker(EventCustomProfileFields) check_custom_profile_fields = make_checker(EventCustomProfileFields)
check_default_stream_groups = make_checker(EventDefaultStreamGroups) check_default_stream_groups = make_checker(EventDefaultStreamGroups)
check_default_streams = make_checker(EventDefaultStreams) check_default_streams = make_checker(EventDefaultStreams)

View File

@@ -66,6 +66,22 @@ class EventAttachmentUpdate(BaseEvent):
upload_space_used: int upload_space_used: int
class ChannelFolderForEventChannelFolderAdd(BaseModel):
id: int
name: str
description: str
rendered_description: str
date_created: int
creator_id: int
is_archived: bool
class EventChannelFolderAdd(BaseEvent):
type: Literal["channel_folder"]
op: Literal["add"]
channel_folder: ChannelFolderForEventChannelFolderAdd
class DetailedCustomProfileCore(BaseModel): class DetailedCustomProfileCore(BaseModel):
id: int id: int
type: int type: int

View File

@@ -19,6 +19,7 @@ from zerver.lib import emoji
from zerver.lib.alert_words import user_alert_words from zerver.lib.alert_words import user_alert_words
from zerver.lib.avatar import avatar_url from zerver.lib.avatar import avatar_url
from zerver.lib.bot_config import load_bot_config_template from zerver.lib.bot_config import load_bot_config_template
from zerver.lib.channel_folders import get_channel_folders_in_realm
from zerver.lib.compatibility import is_outdated_server from zerver.lib.compatibility import is_outdated_server
from zerver.lib.default_streams import get_default_stream_ids_for_realm from zerver.lib.default_streams import get_default_stream_ids_for_realm
from zerver.lib.exceptions import JsonableError from zerver.lib.exceptions import JsonableError
@@ -764,6 +765,11 @@ def fetch_initial_state_data(
state["unsubscribed"] = sub_info.unsubscribed state["unsubscribed"] = sub_info.unsubscribed
state["never_subscribed"] = sub_info.never_subscribed state["never_subscribed"] = sub_info.never_subscribed
if want("channel_folders") and user_profile is not None:
# TODO: Spectators should get the channel folders that
# contain atleast one web-public channel.
state["channel_folders"] = get_channel_folders_in_realm(user_profile.realm)
if want("update_message_flags") and want("message"): if want("update_message_flags") and want("message"):
# Keeping unread_msgs updated requires both message flag updates and # Keeping unread_msgs updated requires both message flag updates and
# message updates. This is due to the fact that new messages will not # message updates. This is due to the fact that new messages will not
@@ -1870,6 +1876,12 @@ def apply_event(
else: else:
fields = ["stream_id", "topic_name", "visibility_policy", "last_updated"] fields = ["stream_id", "topic_name", "visibility_policy", "last_updated"]
state["user_topics"].append({x: event[x] for x in fields}) state["user_topics"].append({x: event[x] for x in fields})
elif event["type"] == "channel_folder":
if event["op"] == "add":
state["channel_folders"].append(event["channel_folder"])
state["channel_folders"].sort(key=lambda folder: folder["id"])
else:
raise AssertionError("Unexpected event type {type}/{op}".format(**event))
elif event["type"] == "has_zoom_token": elif event["type"] == "has_zoom_token":
state["has_zoom_token"] = event["value"] state["has_zoom_token"] = event["value"]
elif event["type"] == "web_reload_client": elif event["type"] == "web_reload_client":

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.1.8 on 2025-05-05 13:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0706_channelfolder"),
]
operations = [
migrations.AddField(
model_name="realmauditlog",
name="modified_channel_folder",
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.CASCADE, to="zerver.channelfolder"
),
),
]

View File

@@ -5,6 +5,7 @@ from django.db import models
from django.db.models import CASCADE, Q from django.db.models import CASCADE, Q
from typing_extensions import override from typing_extensions import override
from zerver.models.channel_folders import ChannelFolder
from zerver.models.groups import NamedUserGroup from zerver.models.groups import NamedUserGroup
from zerver.models.realms import Realm from zerver.models.realms import Realm
from zerver.models.streams import Stream from zerver.models.streams import Stream
@@ -113,6 +114,8 @@ class AuditLogEventType(IntEnum):
SAVED_SNIPPET_CREATED = 800 SAVED_SNIPPET_CREATED = 800
CHANNEL_FOLDER_CREATED = 901
# The following values are only for remote server/realm logs. # The following values are only for remote server/realm logs.
# Values should be exactly 10000 greater than the corresponding # Values should be exactly 10000 greater than the corresponding
# value used for the same purpose in realm audit logs (e.g., # value used for the same purpose in realm audit logs (e.g.,
@@ -233,6 +236,11 @@ class RealmAuditLog(AbstractRealmAuditLog):
null=True, null=True,
on_delete=CASCADE, on_delete=CASCADE,
) )
modified_channel_folder = models.ForeignKey(
ChannelFolder,
null=True,
on_delete=CASCADE,
)
event_last_message_id = models.IntegerField(null=True) event_last_message_id = models.IntegerField(null=True)
class Meta: class Meta:

View File

@@ -5716,6 +5716,42 @@ paths:
"op": "remove", "op": "remove",
"scheduled_message_id": 17, "scheduled_message_id": 17,
} }
- type: object
additionalProperties: false
description: |
Event sent to users in an organization when a channel folder is created.
**Changes**: New in Zulip 11.0 (feature level ZF-392de9).
properties:
id:
$ref: "#/components/schemas/EventIdSchema"
type:
allOf:
- $ref: "#/components/schemas/EventTypeSchema"
- enum:
- channel_folder
op:
type: string
enum:
- add
channel_folder:
$ref: "#/components/schemas/ChannelFolder"
example:
{
"type": "channel_folder",
"op": "add",
"channel_folder":
{
"name": "fronted",
"creator_id": 9,
"date_created": 1717484476,
"description": "Channels for frontend discussions",
"rendered_description": "<p>Channels for frontend discussions</p>",
"id": 2,
"is_archived": false,
},
"id": 0,
}
queue_id: queue_id:
type: string type: string
description: | description: |
@@ -15793,6 +15829,17 @@ paths:
in `can_administer_channel_group` of a channel that they never in `can_administer_channel_group` of a channel that they never
subscribed to, but not an organization administrator, the channel subscribed to, but not an organization administrator, the channel
in question would not be part of this array. in question would not be part of this array.
channel_folders:
type: array
items:
$ref: "#/components/schemas/ChannelFolder"
description: |
Present if `channel_folders` is present in `fetch_event_types`.
An array of dictionaries where each dictionary describes one
of the channel folders in the organization.
**Changes**: New in Zulip 11.0 (feature level ZF-392de9).
unread_msgs: unread_msgs:
type: object type: object
description: | description: |
@@ -22819,6 +22866,76 @@ paths:
**Changes**: New in Zulip 10.0 (feature level 298). Previously, **Changes**: New in Zulip 10.0 (feature level 298). Previously,
this error returned the `"BAD_REQUEST"` code. this error returned the `"BAD_REQUEST"` code.
/channel_folders/create:
post:
operationId: create-channel-folder
summary: Create a channel folder
tags: ["channels"]
description: |
Create a new channel folder, that will be used to organize
channels in left sidebar.
Only organization administrators can create a new channel
folder.
**Changes**: New in Zulip 11.0 (feature level ZF-392de9).
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
name:
description: |
The name of the channel folder.
type: string
example: marketing
description:
description: |
The description of the channel folder.
type: string
example: Channels for marketing.
responses:
"200":
description: |
A success response containing the unique ID of the channel folder.
This field provides a straightforward way to reference the
newly created channel folder.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/JsonSuccessBase"
- required:
- channel_folder_id
additionalProperties: false
properties:
result: {}
msg: {}
ignored_parameters_unsupported: {}
channel_folder_id:
type: integer
description: |
The unique ID of the created channel folder.
example:
{"msg": "", "result": "success", "channel_folder_id": 123}
"400":
description: Bad request.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/CodedError"
- example:
{
"result": "error",
"msg": "Must be an organization administrator",
"code": "UNAUTHORIZED_PRINCIPAL",
}
description: |
Error when the user does not have permission
to create a channel folder:
/real-time: /real-time:
# This entry is a hack; it exists to give us a place to put the text # This entry is a hack; it exists to give us a place to put the text
# documenting the parameters for call_on_each_event and friends. # documenting the parameters for call_on_each_event and friends.
@@ -25490,6 +25607,51 @@ components:
This user-generated HTML content should be rendered This user-generated HTML content should be rendered
using the same CSS and client-side security protections using the same CSS and client-side security protections
as are used for message content. as are used for message content.
ChannelFolder:
type: object
additionalProperties: false
description: |
Object containing the channel folder's attributes.
properties:
name:
type: string
description: |
The name of the channel folder.
date_created:
type: integer
nullable: true
description: |
The UNIX timestamp for when the channel folder was created,
in UTC seconds.
creator_id:
type: integer
nullable: true
description: |
The ID of the user who created this channel folder.
description:
type: string
description: |
The description of the channel folder.
rendered_description:
type: string
description: |
The description of the channel folder rendered as HTML,
intended to be used when displaying the channel folder
description in a UI.
One should use the standard Zulip rendered_markdown CSS when
displaying this content so that emoji, LaTeX, and other syntax
work correctly. And any client-side security logic for
user-generated message content should be applied when displaying
this HTML as though it were the body of a Zulip message.
id:
type: integer
description: |
The ID of the channel folder.
is_archived:
type: boolean
description: |
Whether the channel folder is archived or not.
JsonResponseBase: JsonResponseBase:
type: object type: object
properties: properties:

View File

@@ -12,6 +12,7 @@ from zerver.actions.bots import (
do_change_default_events_register_stream, do_change_default_events_register_stream,
do_change_default_sending_stream, do_change_default_sending_stream,
) )
from zerver.actions.channel_folders import check_add_channel_folder
from zerver.actions.create_realm import do_create_realm from zerver.actions.create_realm import do_create_realm
from zerver.actions.create_user import ( from zerver.actions.create_user import (
do_activate_mirror_dummy_user, do_activate_mirror_dummy_user,
@@ -1537,3 +1538,23 @@ class TestRealmAuditLog(ZulipTestCase):
self.assert_length(audit_log_entries, 1) self.assert_length(audit_log_entries, 1)
self.assertIsNone(audit_log_entries[0].modified_user) self.assertIsNone(audit_log_entries[0].modified_user)
self.assertEqual(audit_log_entries[0].modified_user_group, user_group) self.assertEqual(audit_log_entries[0].modified_user_group, user_group)
def test_channel_folders(self) -> None:
iago = self.example_user("iago")
now = timezone_now()
channel_folder = check_add_channel_folder(
"Frontend",
"Channels for frontend discussions",
acting_user=iago,
)
audit_log_entries = RealmAuditLog.objects.filter(
acting_user=iago,
realm=iago.realm,
event_time__gte=now,
event_type=AuditLogEventType.CHANNEL_FOLDER_CREATED,
)
self.assert_length(audit_log_entries, 1)
self.assertIsNone(audit_log_entries[0].modified_user)
self.assertIsNone(audit_log_entries[0].modified_user_group)
self.assertEqual(audit_log_entries[0].modified_channel_folder, channel_folder)

View File

@@ -0,0 +1,72 @@
import orjson
from zerver.lib.test_classes import ZulipTestCase
from zerver.models import ChannelFolder
from zerver.models.realms import get_realm
class ChannelFolderCreationTest(ZulipTestCase):
def test_creating_channel_folder(self) -> None:
self.login("shiva")
realm = get_realm("zulip")
params = {"name": "Frontend", "description": ""}
result = self.client_post("/json/channel_folders/create", params)
self.assert_json_error(result, "Must be an organization administrator")
self.login("iago")
result = self.client_post("/json/channel_folders/create", params)
self.assert_json_success(result)
channel_folder = ChannelFolder.objects.filter(realm=realm).last()
assert channel_folder is not None
self.assertEqual(channel_folder.name, "Frontend")
self.assertEqual(channel_folder.description, "")
response = orjson.loads(result.content)
self.assertEqual(response["channel_folder_id"], channel_folder.id)
def test_creating_channel_folder_with_duplicate_name(self) -> None:
self.login("iago")
realm = get_realm("zulip")
params = {"name": "Frontend", "description": ""}
result = self.client_post("/json/channel_folders/create", params)
self.assert_json_success(result)
self.assertTrue(ChannelFolder.objects.filter(realm=realm, name="Frontend").exists())
result = self.client_post("/json/channel_folders/create", params)
self.assert_json_error(result, "Channel folder 'Frontend' already exists")
def test_rendered_description_for_channel_folder(self) -> None:
self.login("iago")
realm = get_realm("zulip")
params = {"name": "Frontend", "description": "Channels for frontend discussions"}
result = self.client_post("/json/channel_folders/create", params)
self.assert_json_success(result)
channel_folder = ChannelFolder.objects.get(realm=realm, name="Frontend")
self.assertEqual(channel_folder.description, "Channels for frontend discussions")
self.assertEqual(
channel_folder.rendered_description, "<p>Channels for frontend discussions</p>"
)
params = {"name": "Backend", "description": "Channels for **backend** discussions"}
result = self.client_post("/json/channel_folders/create", params)
self.assert_json_success(result)
channel_folder = ChannelFolder.objects.get(realm=realm, name="Backend")
self.assertEqual(channel_folder.description, "Channels for **backend** discussions")
self.assertEqual(
channel_folder.rendered_description,
"<p>Channels for <strong>backend</strong> discussions</p>",
)
def test_invalid_names_for_channel_folder(self) -> None:
self.login("iago")
params = {"name": "", "description": "Channels for frontend discussions"}
result = self.client_post("/json/channel_folders/create", params)
self.assert_json_error(result, "Channel folder name can't be empty.")
invalid_name = "abc\000"
params = {"name": invalid_name, "description": "Channels for frontend discussions"}
result = self.client_post("/json/channel_folders/create", params)
self.assert_json_error(result, "Invalid character in channel folder name, at position 4.")

View File

@@ -1213,13 +1213,14 @@ class FetchQueriesTest(ZulipTestCase):
self.login_user(user) self.login_user(user)
with ( with (
self.assert_database_query_count(44), self.assert_database_query_count(45),
mock.patch("zerver.lib.events.always_want") as want_mock, mock.patch("zerver.lib.events.always_want") as want_mock,
): ):
fetch_initial_state_data(user, realm=user.realm) fetch_initial_state_data(user, realm=user.realm)
expected_counts = dict( expected_counts = dict(
alert_words=1, alert_words=1,
channel_folders=1,
custom_profile_fields=1, custom_profile_fields=1,
default_streams=1, default_streams=1,
default_stream_groups=1, default_stream_groups=1,

View File

@@ -27,6 +27,7 @@ from zerver.actions.bots import (
do_change_default_events_register_stream, do_change_default_events_register_stream,
do_change_default_sending_stream, do_change_default_sending_stream,
) )
from zerver.actions.channel_folders import check_add_channel_folder
from zerver.actions.create_user import do_create_user, do_reactivate_user from zerver.actions.create_user import do_create_user, do_reactivate_user
from zerver.actions.custom_profile_fields import ( from zerver.actions.custom_profile_fields import (
check_remove_custom_profile_field_value, check_remove_custom_profile_field_value,
@@ -154,6 +155,7 @@ from zerver.lib.event_schema import (
check_attachment_add, check_attachment_add,
check_attachment_remove, check_attachment_remove,
check_attachment_update, check_attachment_update,
check_channel_folder_add,
check_custom_profile_fields, check_custom_profile_fields,
check_default_stream_groups, check_default_stream_groups,
check_default_streams, check_default_streams,
@@ -5433,3 +5435,12 @@ class ScheduledMessagesEventsTest(BaseAction):
with self.verify_action() as events: with self.verify_action() as events:
delete_scheduled_message(self.user_profile, scheduled_message_id) delete_scheduled_message(self.user_profile, scheduled_message_id)
check_scheduled_message_remove("events[0]", events[0]) check_scheduled_message_remove("events[0]", events[0])
class ChannelFolderActionTest(BaseAction):
def test_channel_folder_creation_event(self) -> None:
folder_name = "Frontend"
folder_description = "Channels for **frontend** discussions"
with self.verify_action() as events:
check_add_channel_folder(folder_name, folder_description, acting_user=self.user_profile)
check_channel_folder_add("events[0]", events[0])

View File

@@ -77,6 +77,7 @@ class HomeTest(ZulipTestCase):
"can_create_streams", "can_create_streams",
"can_create_web_public_streams", "can_create_web_public_streams",
"can_invite_others_to_realm", "can_invite_others_to_realm",
"channel_folders",
"cross_realm_bots", "cross_realm_bots",
"custom_profile_field_types", "custom_profile_field_types",
"custom_profile_fields", "custom_profile_fields",
@@ -279,7 +280,7 @@ class HomeTest(ZulipTestCase):
# Verify succeeds once logged-in # Verify succeeds once logged-in
with ( with (
self.assert_database_query_count(54), self.assert_database_query_count(55),
patch("zerver.lib.cache.cache_set") as cache_mock, patch("zerver.lib.cache.cache_set") as cache_mock,
): ):
result = self._get_home_page(stream="Denmark") result = self._get_home_page(stream="Denmark")
@@ -587,7 +588,7 @@ class HomeTest(ZulipTestCase):
# Verify number of queries for Realm admin isn't much higher than for normal users. # Verify number of queries for Realm admin isn't much higher than for normal users.
self.login("iago") self.login("iago")
with ( with (
self.assert_database_query_count(53), self.assert_database_query_count(54),
patch("zerver.lib.cache.cache_set") as cache_mock, patch("zerver.lib.cache.cache_set") as cache_mock,
): ):
result = self._get_home_page() result = self._get_home_page()
@@ -619,7 +620,7 @@ class HomeTest(ZulipTestCase):
self._get_home_page() self._get_home_page()
# Then for the second page load, measure the number of queries. # Then for the second page load, measure the number of queries.
with self.assert_database_query_count(49): with self.assert_database_query_count(50):
result = self._get_home_page() result = self._get_home_page()
# Do a sanity check that our new streams were in the payload. # Do a sanity check that our new streams were in the payload.

View File

@@ -0,0 +1,27 @@
from typing import Annotated
from django.http import HttpRequest, HttpResponse
from pydantic import StringConstraints
from zerver.actions.channel_folders import check_add_channel_folder
from zerver.decorator import require_realm_admin
from zerver.lib.channel_folders import check_channel_folder_name
from zerver.lib.response import json_success
from zerver.lib.typed_endpoint import typed_endpoint
from zerver.models.channel_folders import ChannelFolder
from zerver.models.users import UserProfile
@require_realm_admin
@typed_endpoint
def create_channel_folder(
request: HttpRequest,
user_profile: UserProfile,
*,
name: Annotated[str, StringConstraints(max_length=ChannelFolder.MAX_NAME_LENGTH)],
description: Annotated[str, StringConstraints(max_length=ChannelFolder.MAX_DESCRIPTION_LENGTH)],
) -> HttpResponse:
check_channel_folder_name(name, user_profile.realm)
channel_folder = check_add_channel_folder(name, description, acting_user=user_profile)
return json_success(request, data={"channel_folder_id": channel_folder.id})

View File

@@ -45,6 +45,7 @@ from zerver.views.auth import (
start_social_login, start_social_login,
start_social_signup, start_social_signup,
) )
from zerver.views.channel_folders import create_channel_folder
from zerver.views.compatibility import check_global_compatibility from zerver.views.compatibility import check_global_compatibility
from zerver.views.custom_profile_fields import ( from zerver.views.custom_profile_fields import (
create_realm_custom_profile_field, create_realm_custom_profile_field,
@@ -530,6 +531,7 @@ v1_api_and_json_patterns = [
PATCH=update_subscriptions_backend, PATCH=update_subscriptions_backend,
DELETE=remove_subscriptions_backend, DELETE=remove_subscriptions_backend,
), ),
rest_path("channel_folders/create", POST=create_channel_folder),
# topic-muting -> zerver.views.user_topics # topic-muting -> zerver.views.user_topics
# (deprecated and will be removed once clients are migrated to use '/user_topics') # (deprecated and will be removed once clients are migrated to use '/user_topics')
rest_path("users/me/subscriptions/muted_topics", PATCH=update_muted_topic), rest_path("users/me/subscriptions/muted_topics", PATCH=update_muted_topic),