uploads: Implement 5GB/user quota for paid orgs on Zulip Cloud.

Fixes #28621

Till now, this was actually a flat 50GB despite what the /plans/ page
says and was adjusted flexibly when somebody asked for a higher limit.

This actually implements the advertised formula, but changing it to
5GB/user since that's a more reasonable limit.

Keeps the 50GB limit for sponsored Standard Free organizations and also
places it as the floor for the quota for paid orgs, to not lower this
for tiny orgs with less than 5 users.
This commit is contained in:
Mateusz Mandera 2024-03-13 00:09:01 +01:00 committed by Tim Abbott
parent 4eacd25ad5
commit 066de96a86
5 changed files with 51 additions and 17 deletions

View File

@ -304,7 +304,7 @@
<hr />
<ul class="feature-list">
<li>Unlimited search history</li>
<li>File storage up to 10 GB per user</li>
<li>File storage up to 5 GB per user</li>
<li><a href="/help/message-retention-policy">Message retention policies</a></li>
<li>Brand Zulip with your logo</li>
<li>Priority commercial support</li>

View File

@ -67,7 +67,7 @@
<h2>Standard</h2>
<ul class="feature-list">
<li><span>Unlimited search history</span></li>
<li><span>File storage up to 10 GB per user</span></li>
<li><span>File storage up to 5 GB per user</span></li>
<li><span><a href="/help/message-retention-policy">Message retention policies</a></span></li>
<li><span>Brand Zulip with your logo</span></li>
<li><span>Priority commercial support</span></li>

View File

@ -511,7 +511,7 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
]
UPLOAD_QUOTA_LIMITED = 5
UPLOAD_QUOTA_STANDARD = 50
UPLOAD_QUOTA_STANDARD_FREE = 50
custom_upload_quota_gb = models.IntegerField(null=True)
VIDEO_CHAT_PROVIDERS = {
@ -836,17 +836,24 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
if self.custom_upload_quota_gb is not None:
return self.custom_upload_quota_gb
plan_type = self.plan_type
if plan_type == Realm.PLAN_TYPE_PLUS:
return Realm.UPLOAD_QUOTA_STANDARD
elif plan_type == Realm.PLAN_TYPE_STANDARD:
return Realm.UPLOAD_QUOTA_STANDARD
elif plan_type == Realm.PLAN_TYPE_SELF_HOSTED:
if not settings.CORPORATE_ENABLED:
return None
elif plan_type == Realm.PLAN_TYPE_STANDARD_FREE:
return Realm.UPLOAD_QUOTA_STANDARD
elif plan_type == Realm.PLAN_TYPE_LIMITED:
plan_type = self.plan_type
if plan_type == Realm.PLAN_TYPE_SELF_HOSTED: # nocoverage
return None
if plan_type == Realm.PLAN_TYPE_LIMITED:
return Realm.UPLOAD_QUOTA_LIMITED
elif plan_type == Realm.PLAN_TYPE_STANDARD_FREE:
return Realm.UPLOAD_QUOTA_STANDARD_FREE
elif plan_type in [Realm.PLAN_TYPE_STANDARD, Realm.PLAN_TYPE_PLUS]:
from corporate.lib.stripe import get_seat_count
# Paying customers with few users should get a reasonable minimum quota.
return max(
get_seat_count(self) * settings.UPLOAD_QUOTA_PER_USER_GB,
Realm.UPLOAD_QUOTA_STANDARD_FREE,
)
else:
raise AssertionError("Invalid plan type")

View File

@ -5,7 +5,7 @@ import re
import string
from datetime import datetime, timedelta
from typing import Any, Dict, List, Union
from unittest import mock
from unittest import mock, skipUnless
import orjson
from django.conf import settings
@ -15,6 +15,7 @@ from typing_extensions import override
from confirmation.models import Confirmation, create_confirmation_link
from zerver.actions.create_realm import do_change_realm_subdomain, do_create_realm
from zerver.actions.create_user import do_create_user
from zerver.actions.message_send import (
internal_send_huddle_message,
internal_send_private_message,
@ -60,6 +61,9 @@ from zerver.models.realms import get_realm
from zerver.models.streams import get_stream
from zerver.models.users import get_system_bot, get_user_profile_by_id
if settings.ZILENCER_ENABLED:
from corporate.lib.stripe import get_seat_count
class RealmTest(ZulipTestCase):
def assert_user_profile_cache_gets_new_name(
@ -909,8 +913,22 @@ class RealmTest(ZulipTestCase):
self.assertEqual(realm_audit_log.acting_user, iago)
self.assertEqual(realm.org_type, Realm.ORG_TYPES["government"]["id"])
@skipUnless(settings.ZILENCER_ENABLED, "requires zilencer")
def test_change_realm_plan_type(self) -> None:
realm = get_realm("zulip")
# Create additional user, so that the realm has a lot of seats for the purposes
# of upload quota calculation.
for count in range(10):
do_create_user(
f"email{count}@example.com",
f"password {count}",
realm,
"name",
role=UserProfile.ROLE_MEMBER,
acting_user=None,
)
iago = self.example_user("iago")
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_SELF_HOSTED)
self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
@ -938,7 +956,9 @@ class RealmTest(ZulipTestCase):
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD)
self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
self.assertEqual(realm.message_visibility_limit, None)
self.assertEqual(realm.upload_quota_gb, Realm.UPLOAD_QUOTA_STANDARD)
self.assertEqual(
realm.upload_quota_gb, get_seat_count(realm) * settings.UPLOAD_QUOTA_PER_USER_GB
)
everyone_system_group = UserGroup.objects.get(name=SystemGroups.EVERYONE, realm=realm)
self.assertEqual(realm.can_access_all_users_group_id, everyone_system_group.id)
@ -956,7 +976,7 @@ class RealmTest(ZulipTestCase):
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD_FREE)
self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
self.assertEqual(realm.message_visibility_limit, None)
self.assertEqual(realm.upload_quota_gb, Realm.UPLOAD_QUOTA_STANDARD)
self.assertEqual(realm.upload_quota_gb, Realm.UPLOAD_QUOTA_STANDARD_FREE)
do_change_realm_plan_type(realm, Realm.PLAN_TYPE_LIMITED, acting_user=iago)
do_change_realm_plan_type(realm, Realm.PLAN_TYPE_PLUS, acting_user=iago)
@ -964,7 +984,9 @@ class RealmTest(ZulipTestCase):
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_PLUS)
self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
self.assertEqual(realm.message_visibility_limit, None)
self.assertEqual(realm.upload_quota_gb, Realm.UPLOAD_QUOTA_STANDARD)
self.assertEqual(
realm.upload_quota_gb, get_seat_count(realm) * settings.UPLOAD_QUOTA_PER_USER_GB
)
do_change_realm_permission_group_setting(
realm, "can_access_all_users_group", members_system_group, acting_user=None
@ -974,7 +996,9 @@ class RealmTest(ZulipTestCase):
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD)
self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
self.assertEqual(realm.message_visibility_limit, None)
self.assertEqual(realm.upload_quota_gb, Realm.UPLOAD_QUOTA_STANDARD)
self.assertEqual(
realm.upload_quota_gb, get_seat_count(realm) * settings.UPLOAD_QUOTA_PER_USER_GB
)
self.assertEqual(realm.can_access_all_users_group_id, everyone_system_group.id)
# Test that custom_upload_quota_gb overrides the default upload_quota_gb

View File

@ -167,6 +167,9 @@ LOCAL_UPLOADS_DIR: Optional[str] = None
LOCAL_AVATARS_DIR: Optional[str] = None
LOCAL_FILES_DIR: Optional[str] = None
MAX_FILE_UPLOAD_SIZE = 25
# How many GB an organization on a paid plan can upload per user,
# on zulipchat.com.
UPLOAD_QUOTA_PER_USER_GB = 5
# Jitsi Meet video call integration; set to None to disable integration.
JITSI_SERVER_URL: Optional[str] = "https://meet.jit.si"