mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 14:03:30 +00:00
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:
@@ -63,6 +63,7 @@
|
||||
* [Delete a topic](/api/delete-topic)
|
||||
* [Add a default channel](/api/add-default-stream)
|
||||
* [Remove a default channel](/api/remove-default-stream)
|
||||
* [Create a channel folder](/api/create-channel-folder)
|
||||
|
||||
#### Users
|
||||
|
||||
|
||||
8
api_docs/unmerged.d/ZF-392de9.md
Normal file
8
api_docs/unmerged.d/ZF-392de9.md
Normal 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.
|
||||
43
zerver/actions/channel_folders.py
Normal file
43
zerver/actions/channel_folders.py
Normal 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
|
||||
60
zerver/lib/channel_folders.py
Normal file
60
zerver/lib/channel_folders.py
Normal 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"])
|
||||
@@ -23,6 +23,7 @@ from zerver.lib.event_types import (
|
||||
EventAttachmentAdd,
|
||||
EventAttachmentRemove,
|
||||
EventAttachmentUpdate,
|
||||
EventChannelFolderAdd,
|
||||
EventCustomProfileFields,
|
||||
EventDefaultStreamGroups,
|
||||
EventDefaultStreams,
|
||||
@@ -161,6 +162,7 @@ check_alert_words = make_checker(EventAlertWords)
|
||||
check_attachment_add = make_checker(EventAttachmentAdd)
|
||||
check_attachment_remove = make_checker(EventAttachmentRemove)
|
||||
check_attachment_update = make_checker(EventAttachmentUpdate)
|
||||
check_channel_folder_add = make_checker(EventChannelFolderAdd)
|
||||
check_custom_profile_fields = make_checker(EventCustomProfileFields)
|
||||
check_default_stream_groups = make_checker(EventDefaultStreamGroups)
|
||||
check_default_streams = make_checker(EventDefaultStreams)
|
||||
|
||||
@@ -66,6 +66,22 @@ class EventAttachmentUpdate(BaseEvent):
|
||||
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):
|
||||
id: int
|
||||
type: int
|
||||
|
||||
@@ -19,6 +19,7 @@ from zerver.lib import emoji
|
||||
from zerver.lib.alert_words import user_alert_words
|
||||
from zerver.lib.avatar import avatar_url
|
||||
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.default_streams import get_default_stream_ids_for_realm
|
||||
from zerver.lib.exceptions import JsonableError
|
||||
@@ -764,6 +765,11 @@ def fetch_initial_state_data(
|
||||
state["unsubscribed"] = sub_info.unsubscribed
|
||||
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"):
|
||||
# Keeping unread_msgs updated requires both message flag updates and
|
||||
# message updates. This is due to the fact that new messages will not
|
||||
@@ -1870,6 +1876,12 @@ def apply_event(
|
||||
else:
|
||||
fields = ["stream_id", "topic_name", "visibility_policy", "last_updated"]
|
||||
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":
|
||||
state["has_zoom_token"] = event["value"]
|
||||
elif event["type"] == "web_reload_client":
|
||||
|
||||
@@ -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"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -5,6 +5,7 @@ from django.db import models
|
||||
from django.db.models import CASCADE, Q
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.models.channel_folders import ChannelFolder
|
||||
from zerver.models.groups import NamedUserGroup
|
||||
from zerver.models.realms import Realm
|
||||
from zerver.models.streams import Stream
|
||||
@@ -113,6 +114,8 @@ class AuditLogEventType(IntEnum):
|
||||
|
||||
SAVED_SNIPPET_CREATED = 800
|
||||
|
||||
CHANNEL_FOLDER_CREATED = 901
|
||||
|
||||
# The following values are only for remote server/realm logs.
|
||||
# Values should be exactly 10000 greater than the corresponding
|
||||
# value used for the same purpose in realm audit logs (e.g.,
|
||||
@@ -233,6 +236,11 @@ class RealmAuditLog(AbstractRealmAuditLog):
|
||||
null=True,
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
modified_channel_folder = models.ForeignKey(
|
||||
ChannelFolder,
|
||||
null=True,
|
||||
on_delete=CASCADE,
|
||||
)
|
||||
event_last_message_id = models.IntegerField(null=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -5716,6 +5716,42 @@ paths:
|
||||
"op": "remove",
|
||||
"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:
|
||||
type: string
|
||||
description: |
|
||||
@@ -15793,6 +15829,17 @@ paths:
|
||||
in `can_administer_channel_group` of a channel that they never
|
||||
subscribed to, but not an organization administrator, the channel
|
||||
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:
|
||||
type: object
|
||||
description: |
|
||||
@@ -22819,6 +22866,76 @@ paths:
|
||||
|
||||
**Changes**: New in Zulip 10.0 (feature level 298). Previously,
|
||||
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:
|
||||
# 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.
|
||||
@@ -25490,6 +25607,51 @@ components:
|
||||
This user-generated HTML content should be rendered
|
||||
using the same CSS and client-side security protections
|
||||
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:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -12,6 +12,7 @@ from zerver.actions.bots import (
|
||||
do_change_default_events_register_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_user import (
|
||||
do_activate_mirror_dummy_user,
|
||||
@@ -1537,3 +1538,23 @@ class TestRealmAuditLog(ZulipTestCase):
|
||||
self.assert_length(audit_log_entries, 1)
|
||||
self.assertIsNone(audit_log_entries[0].modified_user)
|
||||
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)
|
||||
|
||||
72
zerver/tests/test_channel_folders.py
Normal file
72
zerver/tests/test_channel_folders.py
Normal 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.")
|
||||
@@ -1213,13 +1213,14 @@ class FetchQueriesTest(ZulipTestCase):
|
||||
self.login_user(user)
|
||||
|
||||
with (
|
||||
self.assert_database_query_count(44),
|
||||
self.assert_database_query_count(45),
|
||||
mock.patch("zerver.lib.events.always_want") as want_mock,
|
||||
):
|
||||
fetch_initial_state_data(user, realm=user.realm)
|
||||
|
||||
expected_counts = dict(
|
||||
alert_words=1,
|
||||
channel_folders=1,
|
||||
custom_profile_fields=1,
|
||||
default_streams=1,
|
||||
default_stream_groups=1,
|
||||
|
||||
@@ -27,6 +27,7 @@ from zerver.actions.bots import (
|
||||
do_change_default_events_register_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.custom_profile_fields import (
|
||||
check_remove_custom_profile_field_value,
|
||||
@@ -154,6 +155,7 @@ from zerver.lib.event_schema import (
|
||||
check_attachment_add,
|
||||
check_attachment_remove,
|
||||
check_attachment_update,
|
||||
check_channel_folder_add,
|
||||
check_custom_profile_fields,
|
||||
check_default_stream_groups,
|
||||
check_default_streams,
|
||||
@@ -5433,3 +5435,12 @@ class ScheduledMessagesEventsTest(BaseAction):
|
||||
with self.verify_action() as events:
|
||||
delete_scheduled_message(self.user_profile, scheduled_message_id)
|
||||
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])
|
||||
|
||||
@@ -77,6 +77,7 @@ class HomeTest(ZulipTestCase):
|
||||
"can_create_streams",
|
||||
"can_create_web_public_streams",
|
||||
"can_invite_others_to_realm",
|
||||
"channel_folders",
|
||||
"cross_realm_bots",
|
||||
"custom_profile_field_types",
|
||||
"custom_profile_fields",
|
||||
@@ -279,7 +280,7 @@ class HomeTest(ZulipTestCase):
|
||||
|
||||
# Verify succeeds once logged-in
|
||||
with (
|
||||
self.assert_database_query_count(54),
|
||||
self.assert_database_query_count(55),
|
||||
patch("zerver.lib.cache.cache_set") as cache_mock,
|
||||
):
|
||||
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.
|
||||
self.login("iago")
|
||||
with (
|
||||
self.assert_database_query_count(53),
|
||||
self.assert_database_query_count(54),
|
||||
patch("zerver.lib.cache.cache_set") as cache_mock,
|
||||
):
|
||||
result = self._get_home_page()
|
||||
@@ -619,7 +620,7 @@ class HomeTest(ZulipTestCase):
|
||||
self._get_home_page()
|
||||
|
||||
# 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()
|
||||
|
||||
# Do a sanity check that our new streams were in the payload.
|
||||
|
||||
27
zerver/views/channel_folders.py
Normal file
27
zerver/views/channel_folders.py
Normal 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})
|
||||
@@ -45,6 +45,7 @@ from zerver.views.auth import (
|
||||
start_social_login,
|
||||
start_social_signup,
|
||||
)
|
||||
from zerver.views.channel_folders import create_channel_folder
|
||||
from zerver.views.compatibility import check_global_compatibility
|
||||
from zerver.views.custom_profile_fields import (
|
||||
create_realm_custom_profile_field,
|
||||
@@ -530,6 +531,7 @@ v1_api_and_json_patterns = [
|
||||
PATCH=update_subscriptions_backend,
|
||||
DELETE=remove_subscriptions_backend,
|
||||
),
|
||||
rest_path("channel_folders/create", POST=create_channel_folder),
|
||||
# topic-muting -> zerver.views.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),
|
||||
|
||||
Reference in New Issue
Block a user