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
|
||||
|
||||
|
||||
class LicenseLimitError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class StripeCardError(BillingError):
|
||||
pass
|
||||
|
||||
|
|
|
@ -104,6 +104,8 @@ function submit_invitation_form() {
|
|||
error_list,
|
||||
is_admin: page_params.is_admin,
|
||||
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");
|
||||
invitee_emails_group.addClass("warning");
|
||||
|
|
|
@ -16,3 +16,18 @@
|
|||
<p id="invitation_non_admin_message">{{t "Organization administrators can reactivate deactivated users." }}</p>
|
||||
{{/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
|
||||
|
||||
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 = (
|
||||
"That user does not exist at MIT or is 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:
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -363,6 +363,17 @@ def notify_new_user(user_profile: UserProfile) -> None:
|
|||
message = _("{user} just signed up for Zulip. (total: {user_count})").format(
|
||||
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)
|
||||
|
||||
# We also send a notification to the Zulip administrative realm
|
||||
|
@ -6637,8 +6648,13 @@ def do_invite_users(
|
|||
streams: Collection[Stream],
|
||||
invite_as: int = PreregistrationUser.INVITE_AS["MEMBER"],
|
||||
) -> 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
|
||||
if not realm.invite_required:
|
||||
|
|
|
@ -362,11 +362,16 @@ class ZephyrMessageAlreadySentException(Exception):
|
|||
|
||||
class InvitationError(JsonableError):
|
||||
code = ErrorCode.INVITATION_FAILED
|
||||
data_fields = ["errors", "sent_invitations"]
|
||||
data_fields = ["errors", "sent_invitations", "license_limit_reached"]
|
||||
|
||||
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:
|
||||
self._msg: str = msg
|
||||
self.errors: List[Tuple[str, str, bool]] = errors
|
||||
self.sent_invitations: bool = sent_invitations
|
||||
self.license_limit_reached: bool = license_limit_reached
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import datetime
|
||||
import random
|
||||
from typing import Sequence
|
||||
from unittest import mock
|
||||
|
||||
import pytz
|
||||
|
@ -6,10 +8,11 @@ from django.conf import settings
|
|||
from django.core import mail
|
||||
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.initial_password import initial_password
|
||||
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
|
||||
|
||||
|
||||
|
@ -252,3 +255,70 @@ class TestNotifyNewUser(ZulipTestCase):
|
|||
f"@_**Cordelia, Lear's daughter|{new_user.id}** just signed up for Zulip.",
|
||||
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,
|
||||
one_click_unsubscribe_link,
|
||||
)
|
||||
from corporate.lib.stripe import get_latest_seat_count
|
||||
from zerver.context_processors import common_context
|
||||
from zerver.decorator import do_two_factor_login
|
||||
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:
|
||||
self.register(self.nonreg_email("test"), "test")
|
||||
# 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
|
||||
# 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)
|
||||
|
||||
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:
|
||||
inviter = self.example_user("hamlet")
|
||||
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}),
|
||||
)
|
||||
|
||||
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):
|
||||
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)
|
||||
|
||||
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:
|
||||
realm = get_realm("zulip")
|
||||
do_set_realm_property(realm, "invite_required", False, acting_user=None)
|
||||
|
|
|
@ -771,7 +771,7 @@ class QueryCountTest(ZulipTestCase):
|
|||
acting_user=None,
|
||||
)
|
||||
|
||||
self.assert_length(queries, 70)
|
||||
self.assert_length(queries, 71)
|
||||
self.assert_length(cache_tries, 22)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
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:
|
||||
confirmation = Confirmation.objects.filter(confirmation_key=confirmation_key).first()
|
||||
|
@ -181,6 +185,12 @@ def accounts_register(request: HttpRequest) -> HttpResponse:
|
|||
except ValidationError:
|
||||
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
|
||||
full_name = None
|
||||
require_ldap_password = False
|
||||
|
|
Loading…
Reference in New Issue