enterprise: licensing fixes (#6601)
* enterprise: fix unique index for key, fix field names Signed-off-by: Jens Langhammer <jens@goauthentik.io> * enterprise: update UI to match Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
b93d1cd008
commit
168423a54e
|
@ -35,13 +35,13 @@ class LicenseSerializer(ModelSerializer):
|
|||
"name",
|
||||
"key",
|
||||
"expiry",
|
||||
"users",
|
||||
"internal_users",
|
||||
"external_users",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"name": {"read_only": True},
|
||||
"expiry": {"read_only": True},
|
||||
"users": {"read_only": True},
|
||||
"internal_users": {"read_only": True},
|
||||
"external_users": {"read_only": True},
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ class LicenseSerializer(ModelSerializer):
|
|||
class LicenseSummary(PassiveSerializer):
|
||||
"""Serializer for license status"""
|
||||
|
||||
users = IntegerField(required=True)
|
||||
internal_users = IntegerField(required=True)
|
||||
external_users = IntegerField(required=True)
|
||||
valid = BooleanField()
|
||||
show_admin_warning = BooleanField()
|
||||
|
@ -62,9 +62,9 @@ class LicenseSummary(PassiveSerializer):
|
|||
class LicenseForecastSerializer(PassiveSerializer):
|
||||
"""Serializer for license forecast"""
|
||||
|
||||
users = IntegerField(required=True)
|
||||
internal_users = IntegerField(required=True)
|
||||
external_users = IntegerField(required=True)
|
||||
forecasted_users = IntegerField(required=True)
|
||||
forecasted_internal_users = IntegerField(required=True)
|
||||
forecasted_external_users = IntegerField(required=True)
|
||||
|
||||
|
||||
|
@ -111,7 +111,7 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
|
|||
latest_valid = datetime.fromtimestamp(total.exp)
|
||||
response = LicenseSummary(
|
||||
data={
|
||||
"users": total.users,
|
||||
"internal_users": total.internal_users,
|
||||
"external_users": total.external_users,
|
||||
"valid": total.is_valid(),
|
||||
"show_admin_warning": show_admin_warning,
|
||||
|
@ -135,8 +135,8 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
|
|||
def forecast(self, request: Request) -> Response:
|
||||
"""Forecast how many users will be required in a year"""
|
||||
last_month = now() - timedelta(days=30)
|
||||
# Forecast for default users
|
||||
users_in_last_month = User.objects.filter(
|
||||
# Forecast for internal users
|
||||
internal_in_last_month = User.objects.filter(
|
||||
type=UserTypes.INTERNAL, date_joined__gte=last_month
|
||||
).count()
|
||||
# Forecast for external users
|
||||
|
@ -144,9 +144,9 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
|
|||
forecast_for_months = 12
|
||||
response = LicenseForecastSerializer(
|
||||
data={
|
||||
"users": LicenseKey.get_default_user_count(),
|
||||
"internal_users": LicenseKey.get_default_user_count(),
|
||||
"external_users": LicenseKey.get_external_user_count(),
|
||||
"forecasted_users": (users_in_last_month * forecast_for_months),
|
||||
"forecasted_internal_users": (internal_in_last_month * forecast_for_months),
|
||||
"forecasted_external_users": (external_in_last_month * forecast_for_months),
|
||||
}
|
||||
)
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# Generated by Django 4.2.4 on 2023-08-23 10:06
|
||||
|
||||
import django.contrib.postgres.indexes
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("authentik_enterprise", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="license",
|
||||
old_name="users",
|
||||
new_name="internal_users",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="license",
|
||||
name="key",
|
||||
field=models.TextField(),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="license",
|
||||
index=django.contrib.postgres.indexes.HashIndex(
|
||||
fields=["key"], name="authentik_e_key_523e13_hash"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -11,6 +11,7 @@ from uuid import uuid4
|
|||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.x509 import Certificate, load_der_x509_certificate, load_pem_x509_certificate
|
||||
from dacite import from_dict
|
||||
from django.contrib.postgres.indexes import HashIndex
|
||||
from django.db import models
|
||||
from django.db.models.query import QuerySet
|
||||
from django.utils.timezone import now
|
||||
|
@ -46,7 +47,7 @@ class LicenseKey:
|
|||
exp: int
|
||||
|
||||
name: str
|
||||
users: int
|
||||
internal_users: int
|
||||
external_users: int
|
||||
flags: list[LicenseFlags] = field(default_factory=list)
|
||||
|
||||
|
@ -87,7 +88,7 @@ class LicenseKey:
|
|||
active_licenses = License.objects.filter(expiry__gte=now())
|
||||
total = LicenseKey(get_license_aud(), 0, "Summarized license", 0, 0)
|
||||
for lic in active_licenses:
|
||||
total.users += lic.users
|
||||
total.internal_users += lic.internal_users
|
||||
total.external_users += lic.external_users
|
||||
exp_ts = int(mktime(lic.expiry.timetuple()))
|
||||
if total.exp == 0:
|
||||
|
@ -123,7 +124,7 @@ class LicenseKey:
|
|||
|
||||
Only checks the current count, no historical data is checked"""
|
||||
default_users = self.get_default_user_count()
|
||||
if default_users > self.users:
|
||||
if default_users > self.internal_users:
|
||||
return False
|
||||
active_users = self.get_external_user_count()
|
||||
if active_users > self.external_users:
|
||||
|
@ -153,11 +154,11 @@ class License(models.Model):
|
|||
"""An authentik enterprise license"""
|
||||
|
||||
license_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
key = models.TextField(unique=True)
|
||||
key = models.TextField()
|
||||
|
||||
name = models.TextField()
|
||||
expiry = models.DateTimeField()
|
||||
users = models.BigIntegerField()
|
||||
internal_users = models.BigIntegerField()
|
||||
external_users = models.BigIntegerField()
|
||||
|
||||
@property
|
||||
|
@ -165,6 +166,9 @@ class License(models.Model):
|
|||
"""Get parsed license status"""
|
||||
return LicenseKey.validate(self.key)
|
||||
|
||||
class Meta:
|
||||
indexes = (HashIndex(fields=("key",)),)
|
||||
|
||||
|
||||
def usage_expiry():
|
||||
"""Keep license usage records for 3 months"""
|
||||
|
|
|
@ -13,6 +13,6 @@ def pre_save_license(sender: type[License], instance: License, **_):
|
|||
"""Extract data from license jwt and save it into model"""
|
||||
status = instance.status
|
||||
instance.name = status.name
|
||||
instance.users = status.users
|
||||
instance.internal_users = status.internal_users
|
||||
instance.external_users = status.external_users
|
||||
instance.expiry = datetime.fromtimestamp(status.exp, tz=get_current_timezone())
|
||||
|
|
|
@ -23,7 +23,7 @@ class TestEnterpriseLicense(TestCase):
|
|||
aud="",
|
||||
exp=_exp,
|
||||
name=generate_id(),
|
||||
users=100,
|
||||
internal_users=100,
|
||||
external_users=100,
|
||||
)
|
||||
),
|
||||
|
@ -32,7 +32,7 @@ class TestEnterpriseLicense(TestCase):
|
|||
"""Check license verification"""
|
||||
lic = License.objects.create(key=generate_id())
|
||||
self.assertTrue(lic.status.is_valid())
|
||||
self.assertEqual(lic.users, 100)
|
||||
self.assertEqual(lic.internal_users, 100)
|
||||
|
||||
def test_invalid(self):
|
||||
"""Test invalid license"""
|
||||
|
@ -46,7 +46,7 @@ class TestEnterpriseLicense(TestCase):
|
|||
aud="",
|
||||
exp=_exp,
|
||||
name=generate_id(),
|
||||
users=100,
|
||||
internal_users=100,
|
||||
external_users=100,
|
||||
)
|
||||
),
|
||||
|
@ -58,7 +58,7 @@ class TestEnterpriseLicense(TestCase):
|
|||
lic2 = License.objects.create(key=generate_id())
|
||||
self.assertTrue(lic2.status.is_valid())
|
||||
total = LicenseKey.get_total()
|
||||
self.assertEqual(total.users, 200)
|
||||
self.assertEqual(total.internal_users, 200)
|
||||
self.assertEqual(total.external_users, 200)
|
||||
self.assertEqual(total.exp, _exp)
|
||||
self.assertTrue(total.is_valid())
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-08-17 17:37+0000\n"
|
||||
"POT-Creation-Date: 2023-08-23 10:04+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -98,125 +98,125 @@ msgstr ""
|
|||
msgid "Users added to this group will be superusers."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:162
|
||||
#: authentik/core/models.py:142
|
||||
msgid "User's display name."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:256 authentik/providers/oauth2/models.py:294
|
||||
#: authentik/core/models.py:268 authentik/providers/oauth2/models.py:294
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:257
|
||||
#: authentik/core/models.py:269
|
||||
msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:270
|
||||
#: authentik/core/models.py:282
|
||||
msgid ""
|
||||
"Flow used for authentication when the associated application is accessed by "
|
||||
"an un-authenticated user."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:280
|
||||
#: authentik/core/models.py:292
|
||||
msgid "Flow used when authorizing this provider."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:292
|
||||
#: authentik/core/models.py:304
|
||||
msgid ""
|
||||
"Accessed from applications; optional backchannel providers for protocols "
|
||||
"like LDAP and SCIM."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:347
|
||||
#: authentik/core/models.py:359
|
||||
msgid "Application's display Name."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:348
|
||||
#: authentik/core/models.py:360
|
||||
msgid "Internal application name, used in URLs."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:360
|
||||
#: authentik/core/models.py:372
|
||||
msgid "Open launch URL in a new browser tab or window."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:424
|
||||
#: authentik/core/models.py:436
|
||||
msgid "Application"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:425
|
||||
#: authentik/core/models.py:437
|
||||
msgid "Applications"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:431
|
||||
#: authentik/core/models.py:443
|
||||
msgid "Use the source-specific identifier"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:433
|
||||
#: authentik/core/models.py:445
|
||||
msgid ""
|
||||
"Link to a user with identical email address. Can have security implications "
|
||||
"when a source doesn't validate email addresses."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:437
|
||||
#: authentik/core/models.py:449
|
||||
msgid ""
|
||||
"Use the user's email address, but deny enrollment when the email address "
|
||||
"already exists."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:440
|
||||
#: authentik/core/models.py:452
|
||||
msgid ""
|
||||
"Link to a user with identical username. Can have security implications when "
|
||||
"a username is used with another source."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:444
|
||||
#: authentik/core/models.py:456
|
||||
msgid ""
|
||||
"Use the user's username, but deny enrollment when the username already "
|
||||
"exists."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:451
|
||||
#: authentik/core/models.py:463
|
||||
msgid "Source's display Name."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:452
|
||||
#: authentik/core/models.py:464
|
||||
msgid "Internal source name, used in URLs."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:471
|
||||
#: authentik/core/models.py:483
|
||||
msgid "Flow to use when authenticating existing users."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:480
|
||||
#: authentik/core/models.py:492
|
||||
msgid "Flow to use when enrolling new users."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:488
|
||||
#: authentik/core/models.py:500
|
||||
msgid ""
|
||||
"How the source determines if an existing user should be authenticated or a "
|
||||
"new user enrolled."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:660
|
||||
#: authentik/core/models.py:672
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:661
|
||||
#: authentik/core/models.py:673
|
||||
msgid "Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:702
|
||||
#: authentik/core/models.py:714
|
||||
msgid "Property Mapping"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:703
|
||||
#: authentik/core/models.py:715
|
||||
msgid "Property Mappings"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:738
|
||||
#: authentik/core/models.py:750
|
||||
msgid "Authenticated Session"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/models.py:739
|
||||
#: authentik/core/models.py:751
|
||||
msgid "Authenticated Sessions"
|
||||
msgstr ""
|
||||
|
||||
|
|
16
schema.yml
16
schema.yml
|
@ -31714,7 +31714,7 @@ components:
|
|||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
users:
|
||||
internal_users:
|
||||
type: integer
|
||||
readOnly: true
|
||||
external_users:
|
||||
|
@ -31723,27 +31723,27 @@ components:
|
|||
required:
|
||||
- expiry
|
||||
- external_users
|
||||
- internal_users
|
||||
- key
|
||||
- license_uuid
|
||||
- name
|
||||
- users
|
||||
LicenseForecast:
|
||||
type: object
|
||||
description: Serializer for license forecast
|
||||
properties:
|
||||
users:
|
||||
internal_users:
|
||||
type: integer
|
||||
external_users:
|
||||
type: integer
|
||||
forecasted_users:
|
||||
forecasted_internal_users:
|
||||
type: integer
|
||||
forecasted_external_users:
|
||||
type: integer
|
||||
required:
|
||||
- external_users
|
||||
- forecasted_external_users
|
||||
- forecasted_users
|
||||
- users
|
||||
- forecasted_internal_users
|
||||
- internal_users
|
||||
LicenseRequest:
|
||||
type: object
|
||||
description: License Serializer
|
||||
|
@ -31757,7 +31757,7 @@ components:
|
|||
type: object
|
||||
description: Serializer for license status
|
||||
properties:
|
||||
users:
|
||||
internal_users:
|
||||
type: integer
|
||||
external_users:
|
||||
type: integer
|
||||
|
@ -31777,11 +31777,11 @@ components:
|
|||
required:
|
||||
- external_users
|
||||
- has_license
|
||||
- internal_users
|
||||
- latest_valid
|
||||
- read_only
|
||||
- show_admin_warning
|
||||
- show_user_warning
|
||||
- users
|
||||
- valid
|
||||
Link:
|
||||
type: object
|
||||
|
|
|
@ -170,11 +170,11 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
|
|||
icon="pf-icon pf-icon-user"
|
||||
header=${msg("Forecast internal users")}
|
||||
subtext=${msg(
|
||||
str`Estimated user count one year from now based on ${this.forecast?.users} current internal users and ${this.forecast?.forecastedUsers} forecasted internal users.`,
|
||||
str`Estimated user count one year from now based on ${this.forecast?.internalUsers} current internal users and ${this.forecast?.forecastedInternalUsers} forecasted internal users.`,
|
||||
)}
|
||||
>
|
||||
~ ${(this.forecast?.users || 0) +
|
||||
(this.forecast?.forecastedUsers || 0)}
|
||||
~ ${(this.forecast?.internalUsers || 0) +
|
||||
(this.forecast?.forecastedInternalUsers || 0)}
|
||||
</ak-aggregate-card>
|
||||
<ak-aggregate-card
|
||||
class="pf-l-grid__item"
|
||||
|
@ -217,10 +217,8 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
|
|||
}
|
||||
return [
|
||||
html`<div>${item.name}</div>`,
|
||||
html`<div>
|
||||
<small>0 / ${item.users}</small>
|
||||
<small>0 / ${item.externalUsers}</small>
|
||||
</div>`,
|
||||
html`<div>${msg(str`Internal: ${item.internalUsers}`)}</div>
|
||||
<div>${msg(str`External: ${item.externalUsers}`)}</div>`,
|
||||
html`<ak-label color=${color}> ${item.expiry?.toLocaleString()} </ak-label>`,
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Update")} </span>
|
||||
|
|
|
@ -5806,7 +5806,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
<source>Forecast internal users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sde9a3f41977ec1f8">
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4557b6b9da258643">
|
||||
<source>Forecast external users</source>
|
||||
|
@ -5888,6 +5888,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s6931695c4f563bc4">
|
||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0dd031b58ed4017c">
|
||||
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s57b07e524f8f5c2a">
|
||||
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -6122,7 +6122,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
<source>Forecast internal users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sde9a3f41977ec1f8">
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4557b6b9da258643">
|
||||
<source>Forecast external users</source>
|
||||
|
@ -6204,6 +6204,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s6931695c4f563bc4">
|
||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0dd031b58ed4017c">
|
||||
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s57b07e524f8f5c2a">
|
||||
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -5714,7 +5714,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
<source>Forecast internal users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sde9a3f41977ec1f8">
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4557b6b9da258643">
|
||||
<source>Forecast external users</source>
|
||||
|
@ -5796,6 +5796,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s6931695c4f563bc4">
|
||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0dd031b58ed4017c">
|
||||
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s57b07e524f8f5c2a">
|
||||
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -5821,7 +5821,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
<source>Forecast internal users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sde9a3f41977ec1f8">
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4557b6b9da258643">
|
||||
<source>Forecast external users</source>
|
||||
|
@ -5903,6 +5903,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s6931695c4f563bc4">
|
||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0dd031b58ed4017c">
|
||||
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s57b07e524f8f5c2a">
|
||||
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -5953,7 +5953,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
<source>Forecast internal users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sde9a3f41977ec1f8">
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4557b6b9da258643">
|
||||
<source>Forecast external users</source>
|
||||
|
@ -6035,6 +6035,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s6931695c4f563bc4">
|
||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0dd031b58ed4017c">
|
||||
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s57b07e524f8f5c2a">
|
||||
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -6057,7 +6057,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
<source>Forecast internal users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sde9a3f41977ec1f8">
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4557b6b9da258643">
|
||||
<source>Forecast external users</source>
|
||||
|
@ -6139,6 +6139,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s6931695c4f563bc4">
|
||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0dd031b58ed4017c">
|
||||
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s57b07e524f8f5c2a">
|
||||
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -5704,7 +5704,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
<source>Forecast internal users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sde9a3f41977ec1f8">
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4557b6b9da258643">
|
||||
<source>Forecast external users</source>
|
||||
|
@ -5786,6 +5786,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s6931695c4f563bc4">
|
||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0dd031b58ed4017c">
|
||||
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s57b07e524f8f5c2a">
|
||||
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -7658,8 +7658,8 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
<target>预测内部用户</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sde9a3f41977ec1f8">
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
||||
<target>根据当前 <x id="0" equiv-text="${this.forecast?.users}"/> 名内部用户和 <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> 名预测的内部用户,估算从此时起一年后的用户数。</target>
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||
<target>根据当前 <x id="0" equiv-text="${this.forecast?.internalUsers}"/> 名内部用户和 <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> 名预测的内部用户,估算从此时起一年后的用户数。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4557b6b9da258643">
|
||||
<source>Forecast external users</source>
|
||||
|
@ -7765,6 +7765,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s6931695c4f563bc4">
|
||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0dd031b58ed4017c">
|
||||
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s57b07e524f8f5c2a">
|
||||
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -5759,7 +5759,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
<source>Forecast internal users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sde9a3f41977ec1f8">
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4557b6b9da258643">
|
||||
<source>Forecast external users</source>
|
||||
|
@ -5841,6 +5841,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s6931695c4f563bc4">
|
||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0dd031b58ed4017c">
|
||||
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s57b07e524f8f5c2a">
|
||||
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
|
@ -5758,7 +5758,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
<source>Forecast internal users</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sde9a3f41977ec1f8">
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.users}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedUsers}"/> forecasted internal users.</source>
|
||||
<source>Estimated user count one year from now based on <x id="0" equiv-text="${this.forecast?.internalUsers}"/> current internal users and <x id="1" equiv-text="${this.forecast?.forecastedInternalUsers}"/> forecasted internal users.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4557b6b9da258643">
|
||||
<source>Forecast external users</source>
|
||||
|
@ -5840,6 +5840,12 @@ Bindings to groups/users are checked against the user of the event.</source>
|
|||
</trans-unit>
|
||||
<trans-unit id="s6931695c4f563bc4">
|
||||
<source>The length of the individual generated tokens. Can be increased to improve security.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0dd031b58ed4017c">
|
||||
<source>Internal: <x id="0" equiv-text="${item.internalUsers}"/></source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s57b07e524f8f5c2a">
|
||||
<source>External: <x id="0" equiv-text="${item.externalUsers}"/></source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
|
Reference in New Issue