remote_realm: Add syncing of org_type.

This commit is contained in:
Mateusz Mandera 2023-11-27 02:06:23 +01:00 committed by Tim Abbott
parent e276812e42
commit 02d5740f0f
7 changed files with 108 additions and 16 deletions

View File

@ -7,14 +7,14 @@ import requests
from django.conf import settings from django.conf import settings
from django.forms.models import model_to_dict from django.forms.models import model_to_dict
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from pydantic import UUID4, BaseModel, ConfigDict from pydantic import UUID4, BaseModel, ConfigDict, field_validator
from analytics.models import InstallationCount, RealmCount from analytics.models import InstallationCount, RealmCount
from version import ZULIP_VERSION from version import ZULIP_VERSION
from zerver.lib.exceptions import JsonableError, MissingRemoteRealmError from zerver.lib.exceptions import JsonableError, MissingRemoteRealmError
from zerver.lib.export import floatify_datetime_fields from zerver.lib.export import floatify_datetime_fields
from zerver.lib.outgoing_http import OutgoingSession from zerver.lib.outgoing_http import OutgoingSession
from zerver.models import Realm, RealmAuditLog from zerver.models import OrgTypeEnum, Realm, RealmAuditLog
class PushBouncerSession(OutgoingSession): class PushBouncerSession(OutgoingSession):
@ -36,12 +36,21 @@ class RealmDataForAnalytics(BaseModel):
id: int id: int
host: str host: str
url: str url: str
org_type: int = 0
date_created: float date_created: float
deactivated: bool deactivated: bool
uuid: UUID4 uuid: UUID4
uuid_owner_secret: str uuid_owner_secret: str
@field_validator("org_type")
@classmethod
def check_is_allowed_value(cls, value: int) -> int:
if value not in [org_type.value for org_type in OrgTypeEnum]:
raise ValueError("Not a valid org_type value")
return value
class UserDataForRemoteBilling(BaseModel): class UserDataForRemoteBilling(BaseModel):
uuid: UUID4 uuid: UUID4
@ -214,6 +223,7 @@ def get_realms_info_for_push_bouncer(realm_id: Optional[int] = None) -> List[Rea
url=realm.uri, url=realm.uri,
deactivated=realm.deactivated, deactivated=realm.deactivated,
date_created=realm.date_created.timestamp(), date_created=realm.date_created.timestamp(),
org_type=realm.org_type,
) )
for realm in realms for realm in realms
] ]

View File

@ -364,6 +364,9 @@ def parse_value_for_parameter(parameter: FuncParam[T], value: object) -> T:
# This condition matches our StringRequiredConstraint # This condition matches our StringRequiredConstraint
elif error["type"] == "string_too_short" and error["ctx"].get("min_length") == 1: elif error["type"] == "string_too_short" and error["ctx"].get("min_length") == 1:
error_template = _("{var_name} cannot be blank") error_template = _("{var_name} cannot be blank")
elif error["type"] == "value_error":
context["msg"] = error["msg"]
error_template = _("Invalid {var_name}: {msg}")
assert error_template is not None, MISSING_ERROR_TEMPLATE.format( assert error_template is not None, MISSING_ERROR_TEMPLATE.format(
error_type=error["type"], error_type=error["type"],

View File

@ -8,6 +8,7 @@ import time
from collections import defaultdict from collections import defaultdict
from datetime import timedelta from datetime import timedelta
from email.headerregistry import Address from email.headerregistry import Address
from enum import Enum
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
@ -295,6 +296,22 @@ def generate_realm_uuid_owner_secret() -> str:
return f"zuliprealm_{token}" return f"zuliprealm_{token}"
class OrgTypeEnum(Enum):
Unspecified = 0
Business = 10
OpenSource = 20
EducationNonProfit = 30
Education = 35
Research = 40
Event = 50
NonProfit = 60
Government = 70
PoliticalGroup = 80
Community = 90
Personal = 100
Other = 1000
class OrgTypeDict(TypedDict): class OrgTypeDict(TypedDict):
name: str name: str
id: int id: int
@ -565,91 +582,91 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
ORG_TYPES: Dict[str, OrgTypeDict] = { ORG_TYPES: Dict[str, OrgTypeDict] = {
"unspecified": { "unspecified": {
"name": "Unspecified", "name": "Unspecified",
"id": 0, "id": OrgTypeEnum.Unspecified.value,
"hidden": True, "hidden": True,
"display_order": 0, "display_order": 0,
"onboarding_zulip_guide_url": None, "onboarding_zulip_guide_url": None,
}, },
"business": { "business": {
"name": "Business", "name": "Business",
"id": 10, "id": OrgTypeEnum.Business.value,
"hidden": False, "hidden": False,
"display_order": 1, "display_order": 1,
"onboarding_zulip_guide_url": "https://zulip.com/for/business/", "onboarding_zulip_guide_url": "https://zulip.com/for/business/",
}, },
"opensource": { "opensource": {
"name": "Open-source project", "name": "Open-source project",
"id": 20, "id": OrgTypeEnum.OpenSource.value,
"hidden": False, "hidden": False,
"display_order": 2, "display_order": 2,
"onboarding_zulip_guide_url": "https://zulip.com/for/open-source/", "onboarding_zulip_guide_url": "https://zulip.com/for/open-source/",
}, },
"education_nonprofit": { "education_nonprofit": {
"name": "Education (non-profit)", "name": "Education (non-profit)",
"id": 30, "id": OrgTypeEnum.EducationNonProfit.value,
"hidden": False, "hidden": False,
"display_order": 3, "display_order": 3,
"onboarding_zulip_guide_url": "https://zulip.com/for/education/", "onboarding_zulip_guide_url": "https://zulip.com/for/education/",
}, },
"education": { "education": {
"name": "Education (for-profit)", "name": "Education (for-profit)",
"id": 35, "id": OrgTypeEnum.Education.value,
"hidden": False, "hidden": False,
"display_order": 4, "display_order": 4,
"onboarding_zulip_guide_url": "https://zulip.com/for/education/", "onboarding_zulip_guide_url": "https://zulip.com/for/education/",
}, },
"research": { "research": {
"name": "Research", "name": "Research",
"id": 40, "id": OrgTypeEnum.Research.value,
"hidden": False, "hidden": False,
"display_order": 5, "display_order": 5,
"onboarding_zulip_guide_url": "https://zulip.com/for/research/", "onboarding_zulip_guide_url": "https://zulip.com/for/research/",
}, },
"event": { "event": {
"name": "Event or conference", "name": "Event or conference",
"id": 50, "id": OrgTypeEnum.Event.value,
"hidden": False, "hidden": False,
"display_order": 6, "display_order": 6,
"onboarding_zulip_guide_url": "https://zulip.com/for/events/", "onboarding_zulip_guide_url": "https://zulip.com/for/events/",
}, },
"nonprofit": { "nonprofit": {
"name": "Non-profit (registered)", "name": "Non-profit (registered)",
"id": 60, "id": OrgTypeEnum.NonProfit.value,
"hidden": False, "hidden": False,
"display_order": 7, "display_order": 7,
"onboarding_zulip_guide_url": "https://zulip.com/for/communities/", "onboarding_zulip_guide_url": "https://zulip.com/for/communities/",
}, },
"government": { "government": {
"name": "Government", "name": "Government",
"id": 70, "id": OrgTypeEnum.Government.value,
"hidden": False, "hidden": False,
"display_order": 8, "display_order": 8,
"onboarding_zulip_guide_url": None, "onboarding_zulip_guide_url": None,
}, },
"political_group": { "political_group": {
"name": "Political group", "name": "Political group",
"id": 80, "id": OrgTypeEnum.PoliticalGroup.value,
"hidden": False, "hidden": False,
"display_order": 9, "display_order": 9,
"onboarding_zulip_guide_url": None, "onboarding_zulip_guide_url": None,
}, },
"community": { "community": {
"name": "Community", "name": "Community",
"id": 90, "id": OrgTypeEnum.Community.value,
"hidden": False, "hidden": False,
"display_order": 10, "display_order": 10,
"onboarding_zulip_guide_url": "https://zulip.com/for/communities/", "onboarding_zulip_guide_url": "https://zulip.com/for/communities/",
}, },
"personal": { "personal": {
"name": "Personal", "name": "Personal",
"id": 100, "id": OrgTypeEnum.Personal.value,
"hidden": False, "hidden": False,
"display_order": 100, "display_order": 100,
"onboarding_zulip_guide_url": None, "onboarding_zulip_guide_url": None,
}, },
"other": { "other": {
"name": "Other", "name": "Other",
"id": 1000, "id": OrgTypeEnum.Other.value,
"hidden": False, "hidden": False,
"display_order": 1000, "display_order": 1000,
"onboarding_zulip_guide_url": None, "onboarding_zulip_guide_url": None,

View File

@ -979,6 +979,7 @@ class AnalyticsBouncerTest(BouncerTestCase):
"uuid", "uuid",
"uuid_owner_secret", "uuid_owner_secret",
"host", "host",
"org_type",
"realm_date_created", "realm_date_created",
"registration_deactivated", "registration_deactivated",
"realm_deactivated", "realm_deactivated",
@ -991,6 +992,7 @@ class AnalyticsBouncerTest(BouncerTestCase):
"uuid": realm.uuid, "uuid": realm.uuid,
"uuid_owner_secret": realm.uuid_owner_secret, "uuid_owner_secret": realm.uuid_owner_secret,
"host": realm.host, "host": realm.host,
"org_type": realm.org_type,
"realm_date_created": realm.date_created, "realm_date_created": realm.date_created,
"registration_deactivated": False, "registration_deactivated": False,
"realm_deactivated": False, "realm_deactivated": False,
@ -1160,6 +1162,26 @@ class AnalyticsBouncerTest(BouncerTestCase):
# Only the request counts go up -- all of the other rows' duplicates are dropped # Only the request counts go up -- all of the other rows' duplicates are dropped
check_counts(10, 8, 3, 2, 5) check_counts(10, 8, 3, 2, 5)
# Test that only valid org_type values are accepted - integers defined in OrgTypeEnum.
realms_data = [dict(realm) for realm in get_realms_info_for_push_bouncer()]
# Not a valid org_type value:
realms_data[0]["org_type"] = 11
result = self.uuid_post(
self.server_uuid,
"/api/v1/remotes/server/analytics",
{
"realm_counts": orjson.dumps([]).decode(),
"installation_counts": orjson.dumps([]).decode(),
"realmauditlog_rows": orjson.dumps([]).decode(),
"realms": orjson.dumps(realms_data).decode(),
},
subdomain="",
)
self.assert_json_error(
result, 'Invalid realms[0]["org_type"]: Value error, Not a valid org_type value'
)
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
@responses.activate @responses.activate
def test_analytics_api_invalid(self) -> None: def test_analytics_api_invalid(self) -> None:

View File

@ -0,0 +1,34 @@
# Generated by Django 4.2.7 on 2023-11-27 00:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zilencer", "0038_unique_server_remote_id"),
]
operations = [
migrations.AddField(
model_name="remoterealm",
name="org_type",
field=models.PositiveSmallIntegerField(
choices=[
(0, "Unspecified"),
(10, "Business"),
(20, "Open-source project"),
(30, "Education (non-profit)"),
(35, "Education (for-profit)"),
(40, "Research"),
(50, "Event or conference"),
(60, "Non-profit (registered)"),
(70, "Government"),
(80, "Political group"),
(90, "Community"),
(100, "Personal"),
(1000, "Other"),
],
default=0,
),
),
]

View File

@ -12,7 +12,7 @@ from typing_extensions import override
from analytics.models import BaseCount from analytics.models import BaseCount
from zerver.lib.rate_limiter import RateLimitedObject from zerver.lib.rate_limiter import RateLimitedObject
from zerver.lib.rate_limiter import rules as rate_limiter_rules from zerver.lib.rate_limiter import rules as rate_limiter_rules
from zerver.models import AbstractPushDeviceToken, AbstractRealmAuditLog from zerver.models import AbstractPushDeviceToken, AbstractRealmAuditLog, Realm
def get_remote_server_by_uuid(uuid: str) -> "RemoteZulipServer": def get_remote_server_by_uuid(uuid: str) -> "RemoteZulipServer":
@ -104,6 +104,11 @@ class RemoteRealm(models.Model):
# Value obtained's from the remote server's realm.host. # Value obtained's from the remote server's realm.host.
host = models.TextField() host = models.TextField()
org_type = models.PositiveSmallIntegerField(
default=Realm.ORG_TYPES["unspecified"]["id"],
choices=[(t["id"], t["name"]) for t in Realm.ORG_TYPES.values()],
)
# The fields below are analogical to RemoteZulipServer fields. # The fields below are analogical to RemoteZulipServer fields.
last_updated = models.DateTimeField("last updated", auto_now=True) last_updated = models.DateTimeField("last updated", auto_now=True)

View File

@ -542,6 +542,7 @@ def update_remote_realm_data_for_server(
host=realm.host, host=realm.host,
realm_deactivated=realm.deactivated, realm_deactivated=realm.deactivated,
realm_date_created=timestamp_to_datetime(realm.date_created), realm_date_created=timestamp_to_datetime(realm.date_created),
org_type=realm.org_type,
) )
for realm in server_realms_info for realm in server_realms_info
if realm.uuid not in already_registered_uuids if realm.uuid not in already_registered_uuids