mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 22:13:26 +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)
|
* [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
|
||||||
|
|
||||||
|
|||||||
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,
|
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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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 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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
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)
|
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,
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
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_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),
|
||||||
|
|||||||
Reference in New Issue
Block a user