mirror of https://github.com/zulip/zulip.git
support: Add ability to update max daily invitations for realm.
Adds some validation for changing the realm's max invites via the support view so that it is not set below the default max for the realm's plan type, and so that if it's currently set to the default max it's not reset to that same value.
This commit is contained in:
parent
8911347cdb
commit
d708c3c039
|
@ -4,6 +4,7 @@ from unittest import mock
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
import time_machine
|
import time_machine
|
||||||
|
from django.conf import settings
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
|
@ -1158,6 +1159,55 @@ class TestSupportEndpoint(ZulipTestCase):
|
||||||
["Organization type of zulip changed from Business to Government"], result
|
["Organization type of zulip changed from Business to Government"], result
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_change_max_invites(self) -> None:
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
iago = self.example_user("iago")
|
||||||
|
self.login_user(iago)
|
||||||
|
|
||||||
|
self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
|
||||||
|
result = self.client_post(
|
||||||
|
"/activity/support", {"realm_id": f"{realm.id}", "max_invites": "1"}
|
||||||
|
)
|
||||||
|
self.assert_in_success_response(
|
||||||
|
[
|
||||||
|
"Cannot update maximum number of daily invitations for zulip, because 1 is less than the default for the current plan type."
|
||||||
|
],
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
realm.refresh_from_db()
|
||||||
|
self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
|
||||||
|
|
||||||
|
result = self.client_post(
|
||||||
|
"/activity/support", {"realm_id": f"{realm.id}", "max_invites": "700"}
|
||||||
|
)
|
||||||
|
self.assert_in_success_response(
|
||||||
|
["Maximum number of daily invitations for zulip updated to 700."], result
|
||||||
|
)
|
||||||
|
realm.refresh_from_db()
|
||||||
|
self.assertEqual(realm.max_invites, 700)
|
||||||
|
|
||||||
|
result = self.client_post(
|
||||||
|
"/activity/support", {"realm_id": f"{realm.id}", "max_invites": "0"}
|
||||||
|
)
|
||||||
|
self.assert_in_success_response(
|
||||||
|
[
|
||||||
|
"Maximum number of daily invitations for zulip updated to the default for the current plan type."
|
||||||
|
],
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
realm.refresh_from_db()
|
||||||
|
self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
|
||||||
|
|
||||||
|
result = self.client_post(
|
||||||
|
"/activity/support", {"realm_id": f"{realm.id}", "max_invites": "0"}
|
||||||
|
)
|
||||||
|
self.assert_in_success_response(
|
||||||
|
[
|
||||||
|
"Cannot update maximum number of daily invitations for zulip, because the default for the current plan type is already set."
|
||||||
|
],
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
|
||||||
def test_attach_discount(self) -> None:
|
def test_attach_discount(self) -> None:
|
||||||
lear_realm = get_realm("lear")
|
lear_realm = get_realm("lear")
|
||||||
customer = self.create_customer_and_plan(lear_realm, True)
|
customer = self.create_customer_and_plan(lear_realm, True)
|
||||||
|
|
|
@ -46,6 +46,7 @@ from corporate.lib.support import (
|
||||||
from corporate.models import CustomerPlan
|
from corporate.models import CustomerPlan
|
||||||
from zerver.actions.create_realm import do_change_realm_subdomain
|
from zerver.actions.create_realm import do_change_realm_subdomain
|
||||||
from zerver.actions.realm_settings import (
|
from zerver.actions.realm_settings import (
|
||||||
|
do_change_realm_max_invites,
|
||||||
do_change_realm_org_type,
|
do_change_realm_org_type,
|
||||||
do_change_realm_plan_type,
|
do_change_realm_plan_type,
|
||||||
do_deactivate_realm,
|
do_deactivate_realm,
|
||||||
|
@ -312,6 +313,22 @@ def get_realm_plan_type_options_for_discount() -> list[SupportSelectOption]:
|
||||||
return plan_types
|
return plan_types
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_max_invites_for_plan_type(realm: Realm) -> int:
|
||||||
|
if realm.plan_type in [
|
||||||
|
Realm.PLAN_TYPE_PLUS,
|
||||||
|
Realm.PLAN_TYPE_STANDARD,
|
||||||
|
Realm.PLAN_TYPE_STANDARD_FREE,
|
||||||
|
]:
|
||||||
|
return Realm.INVITES_STANDARD_REALM_DAILY_MAX
|
||||||
|
return settings.INVITES_DEFAULT_REALM_DAILY_MAX
|
||||||
|
|
||||||
|
|
||||||
|
def check_update_max_invites(realm: Realm, new_max: int, default_max: int) -> bool:
|
||||||
|
if new_max in [0, default_max]:
|
||||||
|
return realm.max_invites != default_max
|
||||||
|
return new_max > default_max
|
||||||
|
|
||||||
|
|
||||||
VALID_MODIFY_PLAN_METHODS = Literal[
|
VALID_MODIFY_PLAN_METHODS = Literal[
|
||||||
"downgrade_at_billing_cycle_end",
|
"downgrade_at_billing_cycle_end",
|
||||||
"downgrade_now_without_additional_licenses",
|
"downgrade_now_without_additional_licenses",
|
||||||
|
@ -348,6 +365,7 @@ def support(
|
||||||
delete_user_by_id: Json[NonNegativeInt] | None = None,
|
delete_user_by_id: Json[NonNegativeInt] | None = None,
|
||||||
query: Annotated[str | None, ApiParamConfig("q")] = None,
|
query: Annotated[str | None, ApiParamConfig("q")] = None,
|
||||||
org_type: Json[NonNegativeInt] | None = None,
|
org_type: Json[NonNegativeInt] | None = None,
|
||||||
|
max_invites: Json[NonNegativeInt] | None = None,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
context: dict[str, Any] = {}
|
context: dict[str, Any] = {}
|
||||||
|
|
||||||
|
@ -419,6 +437,22 @@ def support(
|
||||||
do_change_realm_org_type(realm, org_type, acting_user=acting_user)
|
do_change_realm_org_type(realm, org_type, acting_user=acting_user)
|
||||||
msg = f"Organization type of {realm.string_id} changed from {get_org_type_display_name(current_realm_type)} to {get_org_type_display_name(org_type)} "
|
msg = f"Organization type of {realm.string_id} changed from {get_org_type_display_name(current_realm_type)} to {get_org_type_display_name(org_type)} "
|
||||||
context["success_message"] = msg
|
context["success_message"] = msg
|
||||||
|
elif max_invites is not None:
|
||||||
|
default_max = get_default_max_invites_for_plan_type(realm)
|
||||||
|
if check_update_max_invites(realm, max_invites, default_max):
|
||||||
|
do_change_realm_max_invites(realm, max_invites, acting_user=acting_user)
|
||||||
|
update_text = str(max_invites)
|
||||||
|
if max_invites == 0:
|
||||||
|
update_text = "the default for the current plan type"
|
||||||
|
msg = f"Maximum number of daily invitations for {realm.string_id} updated to {update_text}."
|
||||||
|
context["success_message"] = msg
|
||||||
|
else:
|
||||||
|
update_text = f"{max_invites} is less than the default for the current plan type"
|
||||||
|
if max_invites in [0, default_max]:
|
||||||
|
update_text = "the default for the current plan type is already set"
|
||||||
|
context["error_message"] = (
|
||||||
|
f"Cannot update maximum number of daily invitations for {realm.string_id}, because {update_text}."
|
||||||
|
)
|
||||||
elif new_subdomain is not None:
|
elif new_subdomain is not None:
|
||||||
old_subdomain = realm.string_id
|
old_subdomain = realm.string_id
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -73,7 +73,9 @@
|
||||||
<button type="submit" class="support-submit-button">Update</button>
|
<button type="submit" class="support-submit-button">Update</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="POST" class="support-form">
|
<form method="POST" class="support-form">
|
||||||
<b>Plan type</b>:<br />
|
<b>Plan type</b>: <i class="fa fa-question-circle-o" data-tippy-content="
|
||||||
|
Will also update maximum daily invitations to be the default value for the new plan type.
|
||||||
|
" data-tippy-maxWidth="auto"></i><br />
|
||||||
{{ csrf_input }}
|
{{ csrf_input }}
|
||||||
<input type="hidden" name="realm_id" value="{{ realm.id }}" />
|
<input type="hidden" name="realm_id" value="{{ realm.id }}" />
|
||||||
<select name="plan_type">
|
<select name="plan_type">
|
||||||
|
@ -87,6 +89,15 @@
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="support-submit-button">Update</button>
|
<button type="submit" class="support-submit-button">Update</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form method="POST" class="support-form">
|
||||||
|
<b>Maximum daily invitations</b>: <i class="fa fa-question-circle-o" data-tippy-content="
|
||||||
|
Setting to zero will attempt to reset this to the default maximum for the current plan type.
|
||||||
|
" data-tippy-maxWidth="auto"></i><br />
|
||||||
|
{{ csrf_input }}
|
||||||
|
<input type="hidden" name="realm_id" value="{{ realm.id }}" />
|
||||||
|
<input type="number" name="max_invites" value="{{ realm.max_invites }}" min="0" required />
|
||||||
|
<button type="submit" class="support-submit-button">Update</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% if realm.plan_type != SPONSORED_PLAN_TYPE %}
|
{% if realm.plan_type != SPONSORED_PLAN_TYPE %}
|
||||||
<div class="support-sponsorship-container">
|
<div class="support-sponsorship-container">
|
||||||
|
|
|
@ -697,6 +697,40 @@ def do_change_realm_org_type(
|
||||||
send_event_on_commit(realm, event, active_user_ids(realm.id))
|
send_event_on_commit(realm, event, active_user_ids(realm.id))
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic(durable=True)
|
||||||
|
def do_change_realm_max_invites(realm: Realm, max_invites: int, acting_user: UserProfile) -> None:
|
||||||
|
old_value = realm.max_invites
|
||||||
|
new_max = max_invites
|
||||||
|
|
||||||
|
# Reset to default maximum for plan type
|
||||||
|
if new_max == 0:
|
||||||
|
if realm.plan_type == Realm.PLAN_TYPE_PLUS:
|
||||||
|
new_max = Realm.INVITES_STANDARD_REALM_DAILY_MAX
|
||||||
|
elif realm.plan_type == Realm.PLAN_TYPE_STANDARD:
|
||||||
|
new_max = Realm.INVITES_STANDARD_REALM_DAILY_MAX
|
||||||
|
elif realm.plan_type == Realm.PLAN_TYPE_SELF_HOSTED:
|
||||||
|
new_max = None # type: ignore[assignment] # https://github.com/python/mypy/issues/3004
|
||||||
|
elif realm.plan_type == Realm.PLAN_TYPE_STANDARD_FREE:
|
||||||
|
new_max = Realm.INVITES_STANDARD_REALM_DAILY_MAX
|
||||||
|
elif realm.plan_type == Realm.PLAN_TYPE_LIMITED:
|
||||||
|
new_max = settings.INVITES_DEFAULT_REALM_DAILY_MAX
|
||||||
|
|
||||||
|
realm.max_invites = new_max
|
||||||
|
realm.save(update_fields=["_max_invites"])
|
||||||
|
|
||||||
|
RealmAuditLog.objects.create(
|
||||||
|
event_type=RealmAuditLog.REALM_PROPERTY_CHANGED,
|
||||||
|
realm=realm,
|
||||||
|
event_time=timezone_now(),
|
||||||
|
acting_user=acting_user,
|
||||||
|
extra_data={
|
||||||
|
"old_value": old_value,
|
||||||
|
"new_value": new_max,
|
||||||
|
"property": "max_invites",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic(savepoint=False)
|
@transaction.atomic(savepoint=False)
|
||||||
def do_change_realm_plan_type(
|
def do_change_realm_plan_type(
|
||||||
realm: Realm, plan_type: int, *, acting_user: UserProfile | None
|
realm: Realm, plan_type: int, *, acting_user: UserProfile | None
|
||||||
|
|
|
@ -24,6 +24,7 @@ from zerver.actions.message_send import (
|
||||||
)
|
)
|
||||||
from zerver.actions.realm_settings import (
|
from zerver.actions.realm_settings import (
|
||||||
do_add_deactivated_redirect,
|
do_add_deactivated_redirect,
|
||||||
|
do_change_realm_max_invites,
|
||||||
do_change_realm_org_type,
|
do_change_realm_org_type,
|
||||||
do_change_realm_permission_group_setting,
|
do_change_realm_permission_group_setting,
|
||||||
do_change_realm_plan_type,
|
do_change_realm_plan_type,
|
||||||
|
@ -1015,6 +1016,132 @@ class RealmTest(ZulipTestCase):
|
||||||
self.assertEqual(realm_audit_log.acting_user, iago)
|
self.assertEqual(realm_audit_log.acting_user, iago)
|
||||||
self.assertEqual(realm.org_type, Realm.ORG_TYPES["government"]["id"])
|
self.assertEqual(realm.org_type, Realm.ORG_TYPES["government"]["id"])
|
||||||
|
|
||||||
|
def test_change_realm_max_invites(self) -> None:
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
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)
|
||||||
|
|
||||||
|
do_change_realm_max_invites(realm, 1, acting_user=iago)
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
realm_audit_log = RealmAuditLog.objects.filter(
|
||||||
|
event_type=RealmAuditLog.REALM_PROPERTY_CHANGED
|
||||||
|
).last()
|
||||||
|
assert realm_audit_log is not None
|
||||||
|
expected_extra_data = {
|
||||||
|
"old_value": settings.INVITES_DEFAULT_REALM_DAILY_MAX,
|
||||||
|
"new_value": 1,
|
||||||
|
"property": "max_invites",
|
||||||
|
}
|
||||||
|
self.assertEqual(realm_audit_log.extra_data, expected_extra_data)
|
||||||
|
self.assertEqual(realm_audit_log.acting_user, iago)
|
||||||
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_SELF_HOSTED)
|
||||||
|
self.assertEqual(realm.max_invites, 1)
|
||||||
|
|
||||||
|
do_change_realm_max_invites(realm, 0, acting_user=iago)
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
realm_audit_log = RealmAuditLog.objects.filter(
|
||||||
|
event_type=RealmAuditLog.REALM_PROPERTY_CHANGED
|
||||||
|
).last()
|
||||||
|
assert realm_audit_log is not None
|
||||||
|
expected_extra_data = {"old_value": 1, "new_value": None, "property": "max_invites"}
|
||||||
|
self.assertEqual(realm_audit_log.extra_data, expected_extra_data)
|
||||||
|
self.assertEqual(realm_audit_log.acting_user, iago)
|
||||||
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_SELF_HOSTED)
|
||||||
|
self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
|
||||||
|
|
||||||
|
realm.plan_type = Realm.PLAN_TYPE_PLUS
|
||||||
|
realm.save()
|
||||||
|
|
||||||
|
do_change_realm_max_invites(realm, 0, acting_user=iago)
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
realm_audit_log = RealmAuditLog.objects.filter(
|
||||||
|
event_type=RealmAuditLog.REALM_PROPERTY_CHANGED
|
||||||
|
).last()
|
||||||
|
assert realm_audit_log is not None
|
||||||
|
expected_extra_data = {
|
||||||
|
"old_value": settings.INVITES_DEFAULT_REALM_DAILY_MAX,
|
||||||
|
"new_value": Realm.INVITES_STANDARD_REALM_DAILY_MAX,
|
||||||
|
"property": "max_invites",
|
||||||
|
}
|
||||||
|
self.assertEqual(realm_audit_log.extra_data, expected_extra_data)
|
||||||
|
self.assertEqual(realm_audit_log.acting_user, iago)
|
||||||
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_PLUS)
|
||||||
|
self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
|
||||||
|
|
||||||
|
realm.plan_type = Realm.PLAN_TYPE_LIMITED
|
||||||
|
realm.save()
|
||||||
|
|
||||||
|
do_change_realm_max_invites(realm, 0, acting_user=iago)
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
realm_audit_log = RealmAuditLog.objects.filter(
|
||||||
|
event_type=RealmAuditLog.REALM_PROPERTY_CHANGED
|
||||||
|
).last()
|
||||||
|
assert realm_audit_log is not None
|
||||||
|
expected_extra_data = {
|
||||||
|
"old_value": Realm.INVITES_STANDARD_REALM_DAILY_MAX,
|
||||||
|
"new_value": settings.INVITES_DEFAULT_REALM_DAILY_MAX,
|
||||||
|
"property": "max_invites",
|
||||||
|
}
|
||||||
|
self.assertEqual(realm_audit_log.extra_data, expected_extra_data)
|
||||||
|
self.assertEqual(realm_audit_log.acting_user, iago)
|
||||||
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_LIMITED)
|
||||||
|
self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
|
||||||
|
|
||||||
|
realm.plan_type = Realm.PLAN_TYPE_STANDARD
|
||||||
|
realm.save()
|
||||||
|
|
||||||
|
do_change_realm_max_invites(realm, 0, acting_user=iago)
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
realm_audit_log = RealmAuditLog.objects.filter(
|
||||||
|
event_type=RealmAuditLog.REALM_PROPERTY_CHANGED
|
||||||
|
).last()
|
||||||
|
assert realm_audit_log is not None
|
||||||
|
expected_extra_data = {
|
||||||
|
"old_value": settings.INVITES_DEFAULT_REALM_DAILY_MAX,
|
||||||
|
"new_value": Realm.INVITES_STANDARD_REALM_DAILY_MAX,
|
||||||
|
"property": "max_invites",
|
||||||
|
}
|
||||||
|
self.assertEqual(realm_audit_log.extra_data, expected_extra_data)
|
||||||
|
self.assertEqual(realm_audit_log.acting_user, iago)
|
||||||
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD)
|
||||||
|
self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
|
||||||
|
|
||||||
|
realm.plan_type = Realm.PLAN_TYPE_STANDARD_FREE
|
||||||
|
realm.save()
|
||||||
|
|
||||||
|
do_change_realm_max_invites(realm, 50000, acting_user=iago)
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
realm_audit_log = RealmAuditLog.objects.filter(
|
||||||
|
event_type=RealmAuditLog.REALM_PROPERTY_CHANGED
|
||||||
|
).last()
|
||||||
|
assert realm_audit_log is not None
|
||||||
|
expected_extra_data = {
|
||||||
|
"old_value": Realm.INVITES_STANDARD_REALM_DAILY_MAX,
|
||||||
|
"new_value": 50000,
|
||||||
|
"property": "max_invites",
|
||||||
|
}
|
||||||
|
self.assertEqual(realm_audit_log.extra_data, expected_extra_data)
|
||||||
|
self.assertEqual(realm_audit_log.acting_user, iago)
|
||||||
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD_FREE)
|
||||||
|
self.assertEqual(realm.max_invites, 50000)
|
||||||
|
|
||||||
|
do_change_realm_max_invites(realm, 0, acting_user=iago)
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
realm_audit_log = RealmAuditLog.objects.filter(
|
||||||
|
event_type=RealmAuditLog.REALM_PROPERTY_CHANGED
|
||||||
|
).last()
|
||||||
|
assert realm_audit_log is not None
|
||||||
|
expected_extra_data = {
|
||||||
|
"old_value": 50000,
|
||||||
|
"new_value": Realm.INVITES_STANDARD_REALM_DAILY_MAX,
|
||||||
|
"property": "max_invites",
|
||||||
|
}
|
||||||
|
self.assertEqual(realm_audit_log.extra_data, expected_extra_data)
|
||||||
|
self.assertEqual(realm_audit_log.acting_user, iago)
|
||||||
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD_FREE)
|
||||||
|
self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
|
||||||
|
|
||||||
@skipUnless(settings.ZILENCER_ENABLED, "requires zilencer")
|
@skipUnless(settings.ZILENCER_ENABLED, "requires zilencer")
|
||||||
def test_change_realm_plan_type(self) -> None:
|
def test_change_realm_plan_type(self) -> None:
|
||||||
realm = get_realm("zulip")
|
realm = get_realm("zulip")
|
||||||
|
|
Loading…
Reference in New Issue