diff --git a/zerver/lib/export.py b/zerver/lib/export.py index e3b7ece721..155d05ba70 100644 --- a/zerver/lib/export.py +++ b/zerver/lib/export.py @@ -49,6 +49,7 @@ from zerver.models import ( Attachment, BotConfigData, BotStorageData, + ChannelFolder, Client, CustomProfileField, CustomProfileFieldValue, @@ -147,6 +148,7 @@ ALL_ZULIP_TABLES = { "zerver_botconfigdata", "zerver_botstoragedata", "zerver_channelemailaddress", + "zerver_channelfolder", "zerver_client", "zerver_customprofilefield", "zerver_customprofilefieldvalue", @@ -327,6 +329,7 @@ DATE_FIELDS: dict[TableName, list[Field]] = { "analytics_streamcount": ["end_time"], "analytics_usercount": ["end_time"], "zerver_attachment": ["create_time"], + "zerver_channelfolder": ["date_created"], "zerver_message": ["last_edit_time", "date_sent"], "zerver_muteduser": ["date_muted"], "zerver_realmauditlog": ["event_time"], @@ -1081,6 +1084,13 @@ def get_realm_config() -> Config: custom_process_results=custom_process_subscription_in_realm_config, ) + Config( + table="zerver_channelfolder", + model=ChannelFolder, + normal_parent=realm_config, + include_rows="realm_id__in", + ) + add_user_profile_child_configs(user_profile_config) return realm_config diff --git a/zerver/lib/import_realm.py b/zerver/lib/import_realm.py index 46b088c269..7b3ae3bd7d 100644 --- a/zerver/lib/import_realm.py +++ b/zerver/lib/import_realm.py @@ -62,6 +62,7 @@ from zerver.models import ( Attachment, BotConfigData, BotStorageData, + ChannelFolder, Client, CustomProfileField, CustomProfileFieldValue, @@ -169,6 +170,7 @@ ID_MAP: dict[str, dict[int, int]] = { "scheduledmessage": {}, "onboardingusermessage": {}, "savedsnippet": {}, + "channelfolder": {}, } id_map_to_list: dict[str, dict[int, list[int]]] = { @@ -1231,6 +1233,8 @@ def do_import_realm(import_dir: Path, subdomain: str, processes: int = 1) -> Rea update_model_ids(UserGroup, data, "usergroup") if "zerver_presencesequence" in data: update_model_ids(PresenceSequence, data, "presencesequence") + if "zerver_channelfolder" in data: + update_model_ids(ChannelFolder, data, "channelfolder") # Now we prepare to import the Realm table re_map_foreign_keys(data, "zerver_realm", "moderation_request_channel", related_table="stream") @@ -1386,6 +1390,12 @@ def do_import_realm(import_dir: Path, subdomain: str, processes: int = 1) -> Rea stream.creator_id = stream_id_to_creator_id[stream.id] Stream.objects.bulk_update(streams, ["creator_id"]) + if "zerver_channelfolder" in data: + fix_datetime_fields(data, "zerver_channelfolder") + re_map_foreign_keys(data, "zerver_channelfolder", "realm", related_table="realm") + re_map_foreign_keys(data, "zerver_channelfolder", "creator", related_table="user_profile") + bulk_import_model(data, ChannelFolder) + if "zerver_namedusergroup" in data: # UserProfiles have been loaded, so now we're ready to set .creator_id # for groups based on the mapping we saved earlier. diff --git a/zerver/migrations/0706_channelfolder.py b/zerver/migrations/0706_channelfolder.py new file mode 100644 index 0000000000..a3d87020b8 --- /dev/null +++ b/zerver/migrations/0706_channelfolder.py @@ -0,0 +1,48 @@ +# Generated by Django 5.1.8 on 2025-05-02 09:27 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0705_stream_subscriber_count_data_migration"), + ] + + operations = [ + migrations.CreateModel( + name="ChannelFolder", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("name", models.CharField(max_length=100)), + ("description", models.CharField(default="", max_length=1024)), + ("rendered_description", models.TextField(default="")), + ("date_created", models.DateTimeField(default=django.utils.timezone.now)), + ("is_archived", models.BooleanField(default=False)), + ( + "creator", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "realm", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="zerver.realm" + ), + ), + ], + options={ + "unique_together": {("realm", "name")}, + }, + ), + ] diff --git a/zerver/models/__init__.py b/zerver/models/__init__.py index 73e70fd025..629fb79e35 100644 --- a/zerver/models/__init__.py +++ b/zerver/models/__init__.py @@ -3,6 +3,7 @@ from zerver.models.alert_words import AlertWord as AlertWord from zerver.models.bots import BotConfigData as BotConfigData from zerver.models.bots import BotStorageData as BotStorageData from zerver.models.bots import Service as Service +from zerver.models.channel_folders import ChannelFolder as ChannelFolder from zerver.models.clients import Client as Client from zerver.models.custom_profile_fields import CustomProfileField as CustomProfileField from zerver.models.custom_profile_fields import CustomProfileFieldValue as CustomProfileFieldValue diff --git a/zerver/models/channel_folders.py b/zerver/models/channel_folders.py new file mode 100644 index 0000000000..9a9e61f219 --- /dev/null +++ b/zerver/models/channel_folders.py @@ -0,0 +1,22 @@ +from django.db import models +from django.utils.timezone import now as timezone_now + +from zerver.models.realms import Realm +from zerver.models.users import UserProfile + + +class ChannelFolder(models.Model): + MAX_NAME_LENGTH = 100 + MAX_DESCRIPTION_LENGTH = 1024 + + realm = models.ForeignKey(Realm, on_delete=models.CASCADE) + name = models.CharField(max_length=MAX_NAME_LENGTH) + description = models.CharField(max_length=MAX_DESCRIPTION_LENGTH, default="") + rendered_description = models.TextField(default="") + + date_created = models.DateTimeField(default=timezone_now) + creator = models.ForeignKey(UserProfile, null=True, on_delete=models.SET_NULL) + is_archived = models.BooleanField(default=False) + + class Meta: + unique_together = ("realm", "name") diff --git a/zerver/tests/test_import_export.py b/zerver/tests/test_import_export.py index 0d181b1b68..66391907fd 100644 --- a/zerver/tests/test_import_export.py +++ b/zerver/tests/test_import_export.py @@ -76,6 +76,7 @@ from zerver.models import ( Attachment, BotConfigData, BotStorageData, + ChannelFolder, CustomProfileField, CustomProfileFieldValue, DirectMessageGroup, @@ -1618,6 +1619,13 @@ class RealmImportExportTest(ExportFile): flags=OnboardingUserMessage.flags.starred, ) + ChannelFolder.objects.create( + realm=original_realm, + name="Frontend", + description="Frontend channels", + creator=self.example_user("iago"), + ) + # We want to have an extra, malformed RealmEmoji with no .author # to test that upon import that gets fixed. with get_test_image_file("img.png") as img_file: @@ -2249,6 +2257,10 @@ class RealmImportExportTest(ExportFile): ) return tups + @getter + def get_channel_folders(r: Realm) -> set[str]: + return set(ChannelFolder.objects.filter(realm=r).values_list("name", flat=True)) + return getters def test_import_realm_with_invalid_email_addresses_fails_validation(self) -> None: