mirror of https://github.com/zulip/zulip.git
billing: Enforce license limit for plans on manual license management.
This commit is contained in:
parent
8c055107d9
commit
1938076f67
|
@ -0,0 +1,104 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from corporate.lib.stripe import LicenseLimitError, get_latest_seat_count
|
||||||
|
from corporate.models import get_current_plan_by_realm
|
||||||
|
from zerver.lib.actions import send_message_to_signup_notification_stream
|
||||||
|
from zerver.lib.exceptions import InvitationError
|
||||||
|
from zerver.models import Realm, get_system_bot
|
||||||
|
|
||||||
|
|
||||||
|
def generate_licenses_low_warning_message_if_required(realm: Realm) -> Optional[str]:
|
||||||
|
plan = get_current_plan_by_realm(realm)
|
||||||
|
if plan is None or plan.automanage_licenses:
|
||||||
|
return None
|
||||||
|
|
||||||
|
licenses_remaining = plan.licenses() - get_latest_seat_count(realm)
|
||||||
|
if licenses_remaining > 3:
|
||||||
|
return None
|
||||||
|
|
||||||
|
format_kwargs = {
|
||||||
|
"billing_page_link": "/billing/#settings",
|
||||||
|
"deactivate_user_help_page_link": "/help/deactivate-or-reactivate-a-user",
|
||||||
|
}
|
||||||
|
|
||||||
|
if licenses_remaining <= 0:
|
||||||
|
return _(
|
||||||
|
"Your organization has no Zulip licenses remaining and can no longer accept new users. "
|
||||||
|
"Please [increase the number of licenses]({billing_page_link}) or "
|
||||||
|
"[deactivate inactive users]({deactivate_user_help_page_link}) to allow new users to join."
|
||||||
|
).format(**format_kwargs)
|
||||||
|
|
||||||
|
return {
|
||||||
|
1: _(
|
||||||
|
"Your organization has only one Zulip license remaining. You can "
|
||||||
|
"[increase the number of licenses]({billing_page_link}) or [deactivate inactive users]({deactivate_user_help_page_link}) "
|
||||||
|
"to allow more than one user to join."
|
||||||
|
),
|
||||||
|
2: _(
|
||||||
|
"Your organization has only two Zulip licenses remaining. You can "
|
||||||
|
"[increase the number of licenses]({billing_page_link}) or [deactivate inactive users]({deactivate_user_help_page_link}) "
|
||||||
|
"to allow more than two users to join."
|
||||||
|
),
|
||||||
|
3: _(
|
||||||
|
"Your organization has only three Zulip licenses remaining. You can "
|
||||||
|
"[increase the number of licenses]({billing_page_link}) or [deactivate inactive users]({deactivate_user_help_page_link}) "
|
||||||
|
"to allow more than three users to join."
|
||||||
|
),
|
||||||
|
}[licenses_remaining].format(**format_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def send_user_unable_to_signup_message_to_signup_notification_stream(
|
||||||
|
realm: Realm, user_email: str
|
||||||
|
) -> None:
|
||||||
|
message = _(
|
||||||
|
"A new member ({email}) was unable to join your organization because all Zulip licenses "
|
||||||
|
"are in use. Please [increase the number of licenses]({billing_page_link}) or "
|
||||||
|
"[deactivate inactive users]({deactivate_user_help_page_link}) to allow new members to join."
|
||||||
|
).format(
|
||||||
|
email=user_email,
|
||||||
|
billing_page_link="/billing/#settings",
|
||||||
|
deactivate_user_help_page_link="/help/deactivate-or-reactivate-a-user",
|
||||||
|
)
|
||||||
|
|
||||||
|
send_message_to_signup_notification_stream(
|
||||||
|
get_system_bot(settings.NOTIFICATION_BOT), realm, message
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_spare_licenses_available_for_adding_new_users(
|
||||||
|
realm: Realm, number_of_users_to_add: int
|
||||||
|
) -> None:
|
||||||
|
if realm.string_id in settings.LICENSE_CHECK_EXEMPTED_REALMS_ON_MANUAL_PLAN:
|
||||||
|
return # nocoverage
|
||||||
|
|
||||||
|
plan = get_current_plan_by_realm(realm)
|
||||||
|
if plan is None or plan.automanage_licenses:
|
||||||
|
return
|
||||||
|
if plan.licenses() < get_latest_seat_count(realm) + number_of_users_to_add:
|
||||||
|
raise LicenseLimitError()
|
||||||
|
|
||||||
|
|
||||||
|
def check_spare_licenses_available_for_registering_new_user(
|
||||||
|
realm: Realm, user_email_to_add: str
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
check_spare_licenses_available_for_adding_new_users(realm, 1)
|
||||||
|
except LicenseLimitError:
|
||||||
|
send_user_unable_to_signup_message_to_signup_notification_stream(realm, user_email_to_add)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def check_spare_licenses_available_for_inviting_new_users(realm: Realm, num_invites: int) -> None:
|
||||||
|
try:
|
||||||
|
check_spare_licenses_available_for_adding_new_users(realm, num_invites)
|
||||||
|
except LicenseLimitError:
|
||||||
|
if num_invites == 1:
|
||||||
|
message = _("All Zulip licenses for this organization are currently in use.")
|
||||||
|
else:
|
||||||
|
message = _(
|
||||||
|
"Your organization does not have enough unused Zulip licenses to invite {num_invites} users."
|
||||||
|
).format(num_invites=num_invites)
|
||||||
|
raise InvitationError(message, [], sent_invitations=False, license_limit_reached=True)
|
|
@ -203,6 +203,10 @@ class BillingError(Exception):
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
|
class LicenseLimitError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class StripeCardError(BillingError):
|
class StripeCardError(BillingError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,8 @@ function submit_invitation_form() {
|
||||||
error_list,
|
error_list,
|
||||||
is_admin: page_params.is_admin,
|
is_admin: page_params.is_admin,
|
||||||
is_invitee_deactivated,
|
is_invitee_deactivated,
|
||||||
|
license_limit_reached: arr.license_limit_reached,
|
||||||
|
has_billing_access: page_params.is_owner || page_params.is_billing_admin,
|
||||||
});
|
});
|
||||||
ui_report.message(error_response, invite_status, "alert-warning");
|
ui_report.message(error_response, invite_status, "alert-warning");
|
||||||
invitee_emails_group.addClass("warning");
|
invitee_emails_group.addClass("warning");
|
||||||
|
|
|
@ -16,3 +16,18 @@
|
||||||
<p id="invitation_non_admin_message">{{t "Organization administrators can reactivate deactivated users." }}</p>
|
<p id="invitation_non_admin_message">{{t "Organization administrators can reactivate deactivated users." }}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if license_limit_reached}}
|
||||||
|
{{#if has_billing_access}}
|
||||||
|
{{#tr}}
|
||||||
|
To invite users, please <z-link-billing>increase the number of licenses</z-link-billing> or <z-link-help-page>deactivate inactive users</z-link-help-page>.
|
||||||
|
{{#*inline "z-link-billing"}}<a href="/billing/#settings">{{> @partial-block}}</a>{{/inline}}
|
||||||
|
{{#*inline "z-link-help-page"}}<a href="/help/deactivate-or-reactivate-a-user">{{> @partial-block}}</a>{{/inline}}
|
||||||
|
{{/tr}}
|
||||||
|
{{else}}
|
||||||
|
{{#tr}}
|
||||||
|
Please ask a billing administrator to <z-link-billing>increase the number of licenses</z-link-billing> or <z-link-help-page>deactivate inactive users</z-link-help-page>, and try again.
|
||||||
|
{{#*inline "z-link-billing"}}<a href="/billing/#settings">{{> @partial-block}}</a>{{/inline}}
|
||||||
|
{{#*inline "z-link-help-page"}}<a href="/help/deactivate-or-reactivate-a-user">{{> @partial-block}}</a>{{/inline}}
|
||||||
|
{{/tr}}
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "zerver/portico.html" %}
|
||||||
|
{% block portico_content %}
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<h3>{{ _("This organization cannot accept new members right now.") }}</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{% trans %}
|
||||||
|
New members cannot join this organization because all Zulip licenses are currently in use. Please contact the person who
|
||||||
|
invited you and ask them to increase the number of licenses, then try again.
|
||||||
|
{% endtrans %}
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
|
@ -38,6 +38,10 @@ from zerver.models import (
|
||||||
)
|
)
|
||||||
from zproject.backends import check_password_strength, email_auth_enabled, email_belongs_to_ldap
|
from zproject.backends import check_password_strength, email_auth_enabled, email_belongs_to_ldap
|
||||||
|
|
||||||
|
if settings.BILLING_ENABLED:
|
||||||
|
from corporate.lib.registration import check_spare_licenses_available_for_registering_new_user
|
||||||
|
from corporate.lib.stripe import LicenseLimitError
|
||||||
|
|
||||||
MIT_VALIDATION_ERROR = (
|
MIT_VALIDATION_ERROR = (
|
||||||
"That user does not exist at MIT or is a "
|
"That user does not exist at MIT or is a "
|
||||||
+ '<a href="https://ist.mit.edu/email-lists">mailing list</a>. '
|
+ '<a href="https://ist.mit.edu/email-lists">mailing list</a>. '
|
||||||
|
@ -208,6 +212,17 @@ class HomepageForm(forms.Form):
|
||||||
if realm.is_zephyr_mirror_realm:
|
if realm.is_zephyr_mirror_realm:
|
||||||
email_is_not_mit_mailing_list(email)
|
email_is_not_mit_mailing_list(email)
|
||||||
|
|
||||||
|
if settings.BILLING_ENABLED:
|
||||||
|
try:
|
||||||
|
check_spare_licenses_available_for_registering_new_user(realm, email)
|
||||||
|
except LicenseLimitError:
|
||||||
|
raise ValidationError(
|
||||||
|
_(
|
||||||
|
"New members cannot join this organization because all Zulip licenses are in use. Please contact the person who "
|
||||||
|
"invited you and ask them to increase the number of licenses, then try again."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return email
|
return email
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -363,6 +363,17 @@ def notify_new_user(user_profile: UserProfile) -> None:
|
||||||
message = _("{user} just signed up for Zulip. (total: {user_count})").format(
|
message = _("{user} just signed up for Zulip. (total: {user_count})").format(
|
||||||
user=f"@_**{user_profile.full_name}|{user_profile.id}**", user_count=user_count
|
user=f"@_**{user_profile.full_name}|{user_profile.id}**", user_count=user_count
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if settings.BILLING_ENABLED:
|
||||||
|
from corporate.lib.registration import generate_licenses_low_warning_message_if_required
|
||||||
|
|
||||||
|
licenses_low_warning_message = generate_licenses_low_warning_message_if_required(
|
||||||
|
user_profile.realm
|
||||||
|
)
|
||||||
|
if licenses_low_warning_message is not None:
|
||||||
|
message += "\n"
|
||||||
|
message += licenses_low_warning_message
|
||||||
|
|
||||||
send_message_to_signup_notification_stream(sender, user_profile.realm, message)
|
send_message_to_signup_notification_stream(sender, user_profile.realm, message)
|
||||||
|
|
||||||
# We also send a notification to the Zulip administrative realm
|
# We also send a notification to the Zulip administrative realm
|
||||||
|
@ -6637,8 +6648,13 @@ def do_invite_users(
|
||||||
streams: Collection[Stream],
|
streams: Collection[Stream],
|
||||||
invite_as: int = PreregistrationUser.INVITE_AS["MEMBER"],
|
invite_as: int = PreregistrationUser.INVITE_AS["MEMBER"],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
num_invites = len(invitee_emails)
|
||||||
|
|
||||||
check_invite_limit(user_profile.realm, len(invitee_emails))
|
check_invite_limit(user_profile.realm, num_invites)
|
||||||
|
if settings.BILLING_ENABLED:
|
||||||
|
from corporate.lib.registration import check_spare_licenses_available_for_inviting_new_users
|
||||||
|
|
||||||
|
check_spare_licenses_available_for_inviting_new_users(user_profile.realm, num_invites)
|
||||||
|
|
||||||
realm = user_profile.realm
|
realm = user_profile.realm
|
||||||
if not realm.invite_required:
|
if not realm.invite_required:
|
||||||
|
|
|
@ -362,11 +362,16 @@ class ZephyrMessageAlreadySentException(Exception):
|
||||||
|
|
||||||
class InvitationError(JsonableError):
|
class InvitationError(JsonableError):
|
||||||
code = ErrorCode.INVITATION_FAILED
|
code = ErrorCode.INVITATION_FAILED
|
||||||
data_fields = ["errors", "sent_invitations"]
|
data_fields = ["errors", "sent_invitations", "license_limit_reached"]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, msg: str, errors: List[Tuple[str, str, bool]], sent_invitations: bool
|
self,
|
||||||
|
msg: str,
|
||||||
|
errors: List[Tuple[str, str, bool]],
|
||||||
|
sent_invitations: bool,
|
||||||
|
license_limit_reached: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._msg: str = msg
|
self._msg: str = msg
|
||||||
self.errors: List[Tuple[str, str, bool]] = errors
|
self.errors: List[Tuple[str, str, bool]] = errors
|
||||||
self.sent_invitations: bool = sent_invitations
|
self.sent_invitations: bool = sent_invitations
|
||||||
|
self.license_limit_reached: bool = license_limit_reached
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
import random
|
||||||
|
from typing import Sequence
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
@ -6,10 +8,11 @@ from django.conf import settings
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
|
from corporate.lib.stripe import get_latest_seat_count
|
||||||
from zerver.lib.actions import do_change_notification_settings, notify_new_user
|
from zerver.lib.actions import do_change_notification_settings, notify_new_user
|
||||||
from zerver.lib.initial_password import initial_password
|
from zerver.lib.initial_password import initial_password
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.models import Realm, Recipient, Stream
|
from zerver.models import Realm, Recipient, Stream, UserProfile, get_realm
|
||||||
from zerver.signals import JUST_CREATED_THRESHOLD, get_device_browser, get_device_os
|
from zerver.signals import JUST_CREATED_THRESHOLD, get_device_browser, get_device_os
|
||||||
|
|
||||||
|
|
||||||
|
@ -252,3 +255,70 @@ class TestNotifyNewUser(ZulipTestCase):
|
||||||
f"@_**Cordelia, Lear's daughter|{new_user.id}** just signed up for Zulip.",
|
f"@_**Cordelia, Lear's daughter|{new_user.id}** just signed up for Zulip.",
|
||||||
message.content,
|
message.content,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_notify_realm_of_new_user_in_manual_license_management(self) -> None:
|
||||||
|
stream = self.make_stream(Realm.INITIAL_PRIVATE_STREAM_NAME)
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
realm.signup_notifications_stream_id = stream.id
|
||||||
|
realm.save()
|
||||||
|
|
||||||
|
user_count = get_latest_seat_count(realm)
|
||||||
|
self.subscribe_realm_to_monthly_plan_on_manual_license_management(
|
||||||
|
realm, user_count + 5, user_count + 5
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_new_user_and_verify_strings_in_notification_message(
|
||||||
|
strings_present: Sequence[str] = [], strings_absent: Sequence[str] = []
|
||||||
|
) -> None:
|
||||||
|
user_no = random.randrange(100000)
|
||||||
|
new_user = UserProfile.objects.create(
|
||||||
|
realm=realm,
|
||||||
|
full_name=f"new user {user_no}",
|
||||||
|
email=f"user-{user_no}-email@zulip.com",
|
||||||
|
delivery_email=f"user-{user_no}-delivery-email@zulip.com",
|
||||||
|
)
|
||||||
|
notify_new_user(new_user)
|
||||||
|
|
||||||
|
message = self.get_last_message()
|
||||||
|
actual_stream = Stream.objects.get(id=message.recipient.type_id)
|
||||||
|
self.assertEqual(actual_stream, stream)
|
||||||
|
self.assertIn(
|
||||||
|
f"@_**new user {user_no}|{new_user.id}** just signed up for Zulip.",
|
||||||
|
message.content,
|
||||||
|
)
|
||||||
|
for string_present in strings_present:
|
||||||
|
self.assertIn(
|
||||||
|
string_present,
|
||||||
|
message.content,
|
||||||
|
)
|
||||||
|
for string_absent in strings_absent:
|
||||||
|
self.assertNotIn(string_absent, message.content)
|
||||||
|
|
||||||
|
create_new_user_and_verify_strings_in_notification_message(
|
||||||
|
strings_absent=["Your organization has"]
|
||||||
|
)
|
||||||
|
create_new_user_and_verify_strings_in_notification_message(
|
||||||
|
strings_present=[
|
||||||
|
"Your organization has only three Zulip licenses remaining",
|
||||||
|
"to allow more than three users to",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
create_new_user_and_verify_strings_in_notification_message(
|
||||||
|
strings_present=[
|
||||||
|
"Your organization has only two Zulip licenses remaining",
|
||||||
|
"to allow more than two users to",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
create_new_user_and_verify_strings_in_notification_message(
|
||||||
|
strings_present=[
|
||||||
|
"Your organization has only one Zulip license remaining",
|
||||||
|
"to allow more than one user to",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
create_new_user_and_verify_strings_in_notification_message(
|
||||||
|
strings_present=[
|
||||||
|
"Your organization has no Zulip licenses remaining",
|
||||||
|
"to allow new users to",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
|
@ -27,6 +27,7 @@ from confirmation.models import (
|
||||||
get_object_from_key,
|
get_object_from_key,
|
||||||
one_click_unsubscribe_link,
|
one_click_unsubscribe_link,
|
||||||
)
|
)
|
||||||
|
from corporate.lib.stripe import get_latest_seat_count
|
||||||
from zerver.context_processors import common_context
|
from zerver.context_processors import common_context
|
||||||
from zerver.decorator import do_two_factor_login
|
from zerver.decorator import do_two_factor_login
|
||||||
from zerver.forms import HomepageForm, check_subdomain_available
|
from zerver.forms import HomepageForm, check_subdomain_available
|
||||||
|
@ -773,7 +774,7 @@ class LoginTest(ZulipTestCase):
|
||||||
with queries_captured() as queries, cache_tries_captured() as cache_tries:
|
with queries_captured() as queries, cache_tries_captured() as cache_tries:
|
||||||
self.register(self.nonreg_email("test"), "test")
|
self.register(self.nonreg_email("test"), "test")
|
||||||
# Ensure the number of queries we make is not O(streams)
|
# Ensure the number of queries we make is not O(streams)
|
||||||
self.assert_length(queries, 70)
|
self.assert_length(queries, 73)
|
||||||
|
|
||||||
# We can probably avoid a couple cache hits here, but there doesn't
|
# We can probably avoid a couple cache hits here, but there doesn't
|
||||||
# seem to be any O(N) behavior. Some of the cache hits are related
|
# seem to be any O(N) behavior. Some of the cache hits are related
|
||||||
|
@ -1098,6 +1099,40 @@ class InviteUserTest(InviteUserBase):
|
||||||
|
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
def test_invite_user_to_realm_on_manual_license_plan(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login_user(user)
|
||||||
|
_, ledger = self.subscribe_realm_to_monthly_plan_on_manual_license_management(
|
||||||
|
user.realm, 50, 50
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.settings(BILLING_ENABLED=True):
|
||||||
|
result = self.invite(self.nonreg_email("alice"), ["Denmark"])
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
ledger.licenses_at_next_renewal = 5
|
||||||
|
ledger.save(update_fields=["licenses_at_next_renewal"])
|
||||||
|
with self.settings(BILLING_ENABLED=True):
|
||||||
|
result = self.invite(self.nonreg_email("bob"), ["Denmark"])
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
ledger.licenses = get_latest_seat_count(user.realm) + 1
|
||||||
|
ledger.save(update_fields=["licenses"])
|
||||||
|
with self.settings(BILLING_ENABLED=True):
|
||||||
|
invitee_emails = self.nonreg_email("bob") + "," + self.nonreg_email("alice")
|
||||||
|
result = self.invite(invitee_emails, ["Denmark"])
|
||||||
|
self.assert_json_error_contains(
|
||||||
|
result, "Your organization does not have enough unused Zulip licenses to invite 2 users"
|
||||||
|
)
|
||||||
|
|
||||||
|
ledger.licenses = get_latest_seat_count(user.realm)
|
||||||
|
ledger.save(update_fields=["licenses"])
|
||||||
|
with self.settings(BILLING_ENABLED=True):
|
||||||
|
result = self.invite(self.nonreg_email("bob"), ["Denmark"])
|
||||||
|
self.assert_json_error_contains(
|
||||||
|
result, "All Zulip licenses for this organization are currently in use"
|
||||||
|
)
|
||||||
|
|
||||||
def test_cross_realm_bot(self) -> None:
|
def test_cross_realm_bot(self) -> None:
|
||||||
inviter = self.example_user("hamlet")
|
inviter = self.example_user("hamlet")
|
||||||
self.login_user(inviter)
|
self.login_user(inviter)
|
||||||
|
@ -2004,6 +2039,40 @@ so we didn't send them an invitation. We did send invitations to everyone else!"
|
||||||
reverse("login") + "?" + urlencode({"email": email, "already_registered": 1}),
|
reverse("login") + "?" + urlencode({"email": email, "already_registered": 1}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_confirmation_link_in_manual_license_plan(self) -> None:
|
||||||
|
inviter = self.example_user("iago")
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
|
||||||
|
email = self.nonreg_email("alice")
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
prereg_user = PreregistrationUser.objects.create(
|
||||||
|
email=email, referred_by=inviter, realm=realm
|
||||||
|
)
|
||||||
|
confirmation_link = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
|
||||||
|
registration_key = confirmation_link.split("/")[-1]
|
||||||
|
url = "/accounts/register/"
|
||||||
|
self.client_post(
|
||||||
|
url, {"key": registration_key, "from_confirmation": 1, "full_name": "alice"}
|
||||||
|
)
|
||||||
|
response = self.submit_reg_form_for_user(email, "password", key=registration_key)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "http://zulip.testserver/")
|
||||||
|
|
||||||
|
self.subscribe_realm_to_monthly_plan_on_manual_license_management(realm, 5, 5)
|
||||||
|
|
||||||
|
email = self.nonreg_email("bob")
|
||||||
|
prereg_user = PreregistrationUser.objects.create(
|
||||||
|
email=email, referred_by=inviter, realm=realm
|
||||||
|
)
|
||||||
|
confirmation_link = create_confirmation_link(prereg_user, Confirmation.USER_REGISTRATION)
|
||||||
|
registration_key = confirmation_link.split("/")[-1]
|
||||||
|
url = "/accounts/register/"
|
||||||
|
self.client_post(url, {"key": registration_key, "from_confirmation": 1, "full_name": "bob"})
|
||||||
|
response = self.submit_reg_form_for_user(email, "password", key=registration_key)
|
||||||
|
self.assert_in_success_response(
|
||||||
|
["New members cannot join this organization because all Zulip licenses are"], response
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InvitationsTestCase(InviteUserBase):
|
class InvitationsTestCase(InviteUserBase):
|
||||||
def test_do_get_user_invites(self) -> None:
|
def test_do_get_user_invites(self) -> None:
|
||||||
|
@ -3806,6 +3875,42 @@ class UserSignUpTest(InviteUserBase):
|
||||||
)
|
)
|
||||||
self.assert_in_success_response(["We couldn't find your confirmation link"], result)
|
self.assert_in_success_response(["We couldn't find your confirmation link"], result)
|
||||||
|
|
||||||
|
def test_signup_to_realm_on_manual_license_plan(self) -> None:
|
||||||
|
realm = get_realm("zulip")
|
||||||
|
denmark_stream = get_stream("Denmark", realm)
|
||||||
|
realm.signup_notifications_stream = denmark_stream
|
||||||
|
realm.save(update_fields=["signup_notifications_stream"])
|
||||||
|
|
||||||
|
_, ledger = self.subscribe_realm_to_monthly_plan_on_manual_license_management(realm, 5, 5)
|
||||||
|
|
||||||
|
with self.settings(BILLING_ENABLED=True):
|
||||||
|
form = HomepageForm({"email": self.nonreg_email("test")}, realm=realm)
|
||||||
|
self.assertIn(
|
||||||
|
"New members cannot join this organization because all Zulip licenses",
|
||||||
|
form.errors["email"][0],
|
||||||
|
)
|
||||||
|
last_message = Message.objects.last()
|
||||||
|
self.assertIn(
|
||||||
|
f"A new member ({self.nonreg_email('test')}) was unable to join your organization because all Zulip",
|
||||||
|
last_message.content,
|
||||||
|
)
|
||||||
|
self.assertEqual(last_message.recipient.type_id, denmark_stream.id)
|
||||||
|
|
||||||
|
ledger.licenses_at_next_renewal = 50
|
||||||
|
ledger.save(update_fields=["licenses_at_next_renewal"])
|
||||||
|
with self.settings(BILLING_ENABLED=True):
|
||||||
|
form = HomepageForm({"email": self.nonreg_email("test")}, realm=realm)
|
||||||
|
self.assertIn(
|
||||||
|
"New members cannot join this organization because all Zulip licenses",
|
||||||
|
form.errors["email"][0],
|
||||||
|
)
|
||||||
|
|
||||||
|
ledger.licenses = 50
|
||||||
|
ledger.save(update_fields=["licenses"])
|
||||||
|
with self.settings(BILLING_ENABLED=True):
|
||||||
|
form = HomepageForm({"email": self.nonreg_email("test")}, realm=realm)
|
||||||
|
self.assertEqual(form.errors, {})
|
||||||
|
|
||||||
def test_failed_signup_due_to_restricted_domain(self) -> None:
|
def test_failed_signup_due_to_restricted_domain(self) -> None:
|
||||||
realm = get_realm("zulip")
|
realm = get_realm("zulip")
|
||||||
do_set_realm_property(realm, "invite_required", False, acting_user=None)
|
do_set_realm_property(realm, "invite_required", False, acting_user=None)
|
||||||
|
|
|
@ -771,7 +771,7 @@ class QueryCountTest(ZulipTestCase):
|
||||||
acting_user=None,
|
acting_user=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assert_length(queries, 70)
|
self.assert_length(queries, 71)
|
||||||
self.assert_length(cache_tries, 22)
|
self.assert_length(cache_tries, 22)
|
||||||
|
|
||||||
peer_add_events = [event for event in events if event["event"].get("op") == "peer_add"]
|
peer_add_events = [event for event in events if event["event"].get("op") == "peer_add"]
|
||||||
|
|
|
@ -86,6 +86,10 @@ from zproject.backends import (
|
||||||
password_auth_enabled,
|
password_auth_enabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if settings.BILLING_ENABLED:
|
||||||
|
from corporate.lib.registration import check_spare_licenses_available_for_registering_new_user
|
||||||
|
from corporate.lib.stripe import LicenseLimitError
|
||||||
|
|
||||||
|
|
||||||
def check_prereg_key_and_redirect(request: HttpRequest, confirmation_key: str) -> HttpResponse:
|
def check_prereg_key_and_redirect(request: HttpRequest, confirmation_key: str) -> HttpResponse:
|
||||||
confirmation = Confirmation.objects.filter(confirmation_key=confirmation_key).first()
|
confirmation = Confirmation.objects.filter(confirmation_key=confirmation_key).first()
|
||||||
|
@ -181,6 +185,12 @@ def accounts_register(request: HttpRequest) -> HttpResponse:
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
return redirect_to_email_login_url(email)
|
return redirect_to_email_login_url(email)
|
||||||
|
|
||||||
|
if settings.BILLING_ENABLED:
|
||||||
|
try:
|
||||||
|
check_spare_licenses_available_for_registering_new_user(realm, email)
|
||||||
|
except LicenseLimitError:
|
||||||
|
return render(request, "zerver/no_spare_licenses.html")
|
||||||
|
|
||||||
name_validated = False
|
name_validated = False
|
||||||
full_name = None
|
full_name = None
|
||||||
require_ldap_password = False
|
require_ldap_password = False
|
||||||
|
|
Loading…
Reference in New Issue