mirror of
https://github.com/zulip/zulip.git
synced 2025-11-05 06:23:38 +00:00
compatibility: Shift functions to new module.
Shift functions used for compatibility from zerver.lib.home (is_outdated_server) and zerver.view.compatibility (pop_numerals, version_lt, find_mobile_os, is_outdated_desktop_app, is_unsupported_browser) to zerver.lib.compatibility module.
This commit is contained in:
committed by
Tim Abbott
parent
fcff3cc5da
commit
f82aba5a3d
141
zerver/lib/compatibility.py
Normal file
141
zerver/lib/compatibility.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.timezone import now as timezone_now
|
||||||
|
|
||||||
|
from version import DESKTOP_MINIMUM_VERSION, DESKTOP_WARNING_VERSION
|
||||||
|
from zerver.lib.user_agent import parse_user_agent
|
||||||
|
from zerver.models import UserProfile
|
||||||
|
from zerver.signals import get_device_browser
|
||||||
|
|
||||||
|
# LAST_SERVER_UPGRADE_TIME is the last time the server had a version deployed.
|
||||||
|
if settings.PRODUCTION: # nocoverage
|
||||||
|
timestamp = os.path.basename(os.path.abspath(settings.DEPLOY_ROOT))
|
||||||
|
LAST_SERVER_UPGRADE_TIME = datetime.datetime.strptime(timestamp, "%Y-%m-%d-%H-%M-%S").replace(
|
||||||
|
tzinfo=pytz.utc
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
LAST_SERVER_UPGRADE_TIME = timezone_now()
|
||||||
|
|
||||||
|
|
||||||
|
def is_outdated_server(user_profile: Optional[UserProfile]) -> bool:
|
||||||
|
# Release tarballs are unpacked via `tar -xf`, which means the
|
||||||
|
# `mtime` on files in them is preserved from when the release
|
||||||
|
# tarball was built. Checking this allows us to catch cases where
|
||||||
|
# someone has upgraded in the last year but to a release more than
|
||||||
|
# a year old.
|
||||||
|
git_version_path = os.path.join(settings.DEPLOY_ROOT, "version.py")
|
||||||
|
release_build_time = datetime.datetime.utcfromtimestamp(
|
||||||
|
os.path.getmtime(git_version_path)
|
||||||
|
).replace(tzinfo=pytz.utc)
|
||||||
|
|
||||||
|
version_no_newer_than = min(LAST_SERVER_UPGRADE_TIME, release_build_time)
|
||||||
|
deadline = version_no_newer_than + datetime.timedelta(
|
||||||
|
days=settings.SERVER_UPGRADE_NAG_DEADLINE_DAYS
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_profile is None or not user_profile.is_realm_admin:
|
||||||
|
# Administrators get warned at the deadline; all users 30 days later.
|
||||||
|
deadline = deadline + datetime.timedelta(days=30)
|
||||||
|
|
||||||
|
if timezone_now() > deadline:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def pop_numerals(ver: str) -> Tuple[List[int], str]:
|
||||||
|
match = re.search(r"^( \d+ (?: \. \d+ )* ) (.*)", ver, re.X)
|
||||||
|
if match is None:
|
||||||
|
return [], ver
|
||||||
|
numerals, rest = match.groups()
|
||||||
|
numbers = [int(n) for n in numerals.split(".")]
|
||||||
|
return numbers, rest
|
||||||
|
|
||||||
|
|
||||||
|
def version_lt(ver1: str, ver2: str) -> Optional[bool]:
|
||||||
|
"""
|
||||||
|
Compare two Zulip-style version strings.
|
||||||
|
|
||||||
|
Versions are dot-separated sequences of decimal integers,
|
||||||
|
followed by arbitrary trailing decoration. Comparison is
|
||||||
|
lexicographic on the integer sequences, and refuses to
|
||||||
|
guess how any trailing decoration compares to any other,
|
||||||
|
to further numerals, or to nothing.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if ver1 < ver2
|
||||||
|
False if ver1 >= ver2
|
||||||
|
None if can't tell.
|
||||||
|
"""
|
||||||
|
num1, rest1 = pop_numerals(ver1)
|
||||||
|
num2, rest2 = pop_numerals(ver2)
|
||||||
|
if not num1 or not num2:
|
||||||
|
return None
|
||||||
|
common_len = min(len(num1), len(num2))
|
||||||
|
common_num1, rest_num1 = num1[:common_len], num1[common_len:]
|
||||||
|
common_num2, rest_num2 = num2[:common_len], num2[common_len:]
|
||||||
|
|
||||||
|
# Leading numbers win.
|
||||||
|
if common_num1 != common_num2:
|
||||||
|
return common_num1 < common_num2
|
||||||
|
|
||||||
|
# More numbers beats end-of-string, but ??? vs trailing text.
|
||||||
|
# (NB at most one of rest_num1, rest_num2 is nonempty.)
|
||||||
|
if not rest1 and rest_num2:
|
||||||
|
return True
|
||||||
|
if rest_num1 and not rest2:
|
||||||
|
return False
|
||||||
|
if rest_num1 or rest_num2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Trailing text we can only compare for equality.
|
||||||
|
if rest1 == rest2:
|
||||||
|
return False
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_mobile_os(user_agent: str) -> Optional[str]:
|
||||||
|
if re.search(r"\b Android \b", user_agent, re.I | re.X):
|
||||||
|
return "android"
|
||||||
|
if re.search(r"\b(?: iOS | iPhone\ OS )\b", user_agent, re.I | re.X):
|
||||||
|
return "ios"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_outdated_desktop_app(user_agent_str: str) -> Tuple[bool, bool, bool]:
|
||||||
|
# Returns (insecure, banned, auto_update_broken)
|
||||||
|
user_agent = parse_user_agent(user_agent_str)
|
||||||
|
if user_agent["name"] == "ZulipDesktop":
|
||||||
|
# The deprecated QT/webkit based desktop app, last updated in ~2016.
|
||||||
|
return (True, True, True)
|
||||||
|
|
||||||
|
if user_agent["name"] != "ZulipElectron":
|
||||||
|
return (False, False, False)
|
||||||
|
|
||||||
|
if version_lt(user_agent["version"], "4.0.0"):
|
||||||
|
# Version 2.3.82 and older (aka <4.0.0) of the modern
|
||||||
|
# Electron-based Zulip desktop app with known security issues.
|
||||||
|
# won't auto-update; we may want a special notice to
|
||||||
|
# distinguish those from modern releases.
|
||||||
|
return (True, True, True)
|
||||||
|
|
||||||
|
if version_lt(user_agent["version"], DESKTOP_MINIMUM_VERSION):
|
||||||
|
# Below DESKTOP_MINIMUM_VERSION, we reject access as well.
|
||||||
|
return (True, True, False)
|
||||||
|
|
||||||
|
if version_lt(user_agent["version"], DESKTOP_WARNING_VERSION):
|
||||||
|
# Other insecure versions should just warn.
|
||||||
|
return (True, False, False)
|
||||||
|
|
||||||
|
return (False, False, False)
|
||||||
|
|
||||||
|
|
||||||
|
def is_unsupported_browser(user_agent: str) -> Tuple[bool, Optional[str]]:
|
||||||
|
browser_name = get_device_browser(user_agent)
|
||||||
|
if browser_name == "Internet Explorer":
|
||||||
|
return (True, browser_name)
|
||||||
|
return (False, browser_name)
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
import calendar
|
import calendar
|
||||||
import datetime
|
|
||||||
import os
|
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import pytz
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
from django.utils.timezone import now as timezone_now
|
|
||||||
from two_factor.utils import default_device
|
from two_factor.utils import default_device
|
||||||
|
|
||||||
|
from zerver.lib.compatibility import is_outdated_server
|
||||||
from zerver.lib.events import do_events_register
|
from zerver.lib.events import do_events_register
|
||||||
from zerver.lib.i18n import (
|
from zerver.lib.i18n import (
|
||||||
get_and_set_request_language,
|
get_and_set_request_language,
|
||||||
@@ -39,41 +36,6 @@ class UserPermissionInfo:
|
|||||||
show_webathena: bool
|
show_webathena: bool
|
||||||
|
|
||||||
|
|
||||||
# LAST_SERVER_UPGRADE_TIME is the last time the server had a version deployed.
|
|
||||||
if settings.PRODUCTION: # nocoverage
|
|
||||||
timestamp = os.path.basename(os.path.abspath(settings.DEPLOY_ROOT))
|
|
||||||
LAST_SERVER_UPGRADE_TIME = datetime.datetime.strptime(timestamp, "%Y-%m-%d-%H-%M-%S").replace(
|
|
||||||
tzinfo=pytz.utc
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
LAST_SERVER_UPGRADE_TIME = timezone_now()
|
|
||||||
|
|
||||||
|
|
||||||
def is_outdated_server(user_profile: Optional[UserProfile]) -> bool:
|
|
||||||
# Release tarballs are unpacked via `tar -xf`, which means the
|
|
||||||
# `mtime` on files in them is preserved from when the release
|
|
||||||
# tarball was built. Checking this allows us to catch cases where
|
|
||||||
# someone has upgraded in the last year but to a release more than
|
|
||||||
# a year old.
|
|
||||||
git_version_path = os.path.join(settings.DEPLOY_ROOT, "version.py")
|
|
||||||
release_build_time = datetime.datetime.utcfromtimestamp(
|
|
||||||
os.path.getmtime(git_version_path)
|
|
||||||
).replace(tzinfo=pytz.utc)
|
|
||||||
|
|
||||||
version_no_newer_than = min(LAST_SERVER_UPGRADE_TIME, release_build_time)
|
|
||||||
deadline = version_no_newer_than + datetime.timedelta(
|
|
||||||
days=settings.SERVER_UPGRADE_NAG_DEADLINE_DAYS
|
|
||||||
)
|
|
||||||
|
|
||||||
if user_profile is None or not user_profile.is_realm_admin:
|
|
||||||
# Administrators get warned at the deadline; all users 30 days later.
|
|
||||||
deadline = deadline + datetime.timedelta(days=30)
|
|
||||||
|
|
||||||
if timezone_now() > deadline:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_furthest_read_time(user_profile: Optional[UserProfile]) -> Optional[float]:
|
def get_furthest_read_time(user_profile: Optional[UserProfile]) -> Optional[float]:
|
||||||
if user_profile is None:
|
if user_profile is None:
|
||||||
return time.time()
|
return time.time()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
from zerver.lib.compatibility import find_mobile_os, is_outdated_desktop_app, version_lt
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.views.compatibility import find_mobile_os, is_outdated_desktop_app, version_lt
|
|
||||||
|
|
||||||
|
|
||||||
class VersionTest(ZulipTestCase):
|
class VersionTest(ZulipTestCase):
|
||||||
@@ -109,8 +109,8 @@ class CompatibilityTest(ZulipTestCase):
|
|||||||
else:
|
else:
|
||||||
assert False # nocoverage
|
assert False # nocoverage
|
||||||
|
|
||||||
@mock.patch("zerver.views.compatibility.DESKTOP_MINIMUM_VERSION", "5.0.0")
|
@mock.patch("zerver.lib.compatibility.DESKTOP_MINIMUM_VERSION", "5.0.0")
|
||||||
@mock.patch("zerver.views.compatibility.DESKTOP_WARNING_VERSION", "5.2.0")
|
@mock.patch("zerver.lib.compatibility.DESKTOP_WARNING_VERSION", "5.2.0")
|
||||||
def test_insecure_desktop_app(self) -> None:
|
def test_insecure_desktop_app(self) -> None:
|
||||||
self.assertEqual(is_outdated_desktop_app("ZulipDesktop/0.5.2 (Mac)"), (True, True, True))
|
self.assertEqual(is_outdated_desktop_app("ZulipDesktop/0.5.2 (Mac)"), (True, True, True))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -134,7 +134,7 @@ class CompatibilityTest(ZulipTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Verify what happens if DESKTOP_MINIMUM_VERSION < v < DESKTOP_WARNING_VERSION
|
# Verify what happens if DESKTOP_MINIMUM_VERSION < v < DESKTOP_WARNING_VERSION
|
||||||
with mock.patch("zerver.views.compatibility.DESKTOP_MINIMUM_VERSION", "4.0.3"):
|
with mock.patch("zerver.lib.compatibility.DESKTOP_MINIMUM_VERSION", "4.0.3"):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
is_outdated_desktop_app(
|
is_outdated_desktop_app(
|
||||||
"ZulipElectron/4.0.3 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Zulip/4.0.3 Chrome/66.0.3359.181 Electron/3.1.10 Safari/537.36"
|
"ZulipElectron/4.0.3 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Zulip/4.0.3 Chrome/66.0.3359.181 Electron/3.1.10 Safari/537.36"
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ from zerver.lib.actions import (
|
|||||||
do_change_plan_type,
|
do_change_plan_type,
|
||||||
do_create_user,
|
do_create_user,
|
||||||
)
|
)
|
||||||
|
from zerver.lib.compatibility import LAST_SERVER_UPGRADE_TIME, is_outdated_server
|
||||||
from zerver.lib.events import add_realm_logo_fields
|
from zerver.lib.events import add_realm_logo_fields
|
||||||
from zerver.lib.home import LAST_SERVER_UPGRADE_TIME, get_furthest_read_time, is_outdated_server
|
from zerver.lib.home import get_furthest_read_time
|
||||||
from zerver.lib.soft_deactivation import do_soft_deactivate_users
|
from zerver.lib.soft_deactivation import do_soft_deactivate_users
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.lib.test_helpers import get_user_messages, override_settings, queries_captured
|
from zerver.lib.test_helpers import get_user_messages, override_settings, queries_captured
|
||||||
@@ -896,17 +897,17 @@ class HomeTest(ZulipTestCase):
|
|||||||
hamlet = self.example_user("hamlet")
|
hamlet = self.example_user("hamlet")
|
||||||
iago = self.example_user("iago")
|
iago = self.example_user("iago")
|
||||||
now = LAST_SERVER_UPGRADE_TIME.replace(tzinfo=pytz.utc)
|
now = LAST_SERVER_UPGRADE_TIME.replace(tzinfo=pytz.utc)
|
||||||
with patch("zerver.lib.home.timezone_now", return_value=now + timedelta(days=10)):
|
with patch("zerver.lib.compatibility.timezone_now", return_value=now + timedelta(days=10)):
|
||||||
self.assertEqual(is_outdated_server(iago), False)
|
self.assertEqual(is_outdated_server(iago), False)
|
||||||
self.assertEqual(is_outdated_server(hamlet), False)
|
self.assertEqual(is_outdated_server(hamlet), False)
|
||||||
self.assertEqual(is_outdated_server(None), False)
|
self.assertEqual(is_outdated_server(None), False)
|
||||||
|
|
||||||
with patch("zerver.lib.home.timezone_now", return_value=now + timedelta(days=397)):
|
with patch("zerver.lib.compatibility.timezone_now", return_value=now + timedelta(days=397)):
|
||||||
self.assertEqual(is_outdated_server(iago), True)
|
self.assertEqual(is_outdated_server(iago), True)
|
||||||
self.assertEqual(is_outdated_server(hamlet), True)
|
self.assertEqual(is_outdated_server(hamlet), True)
|
||||||
self.assertEqual(is_outdated_server(None), True)
|
self.assertEqual(is_outdated_server(None), True)
|
||||||
|
|
||||||
with patch("zerver.lib.home.timezone_now", return_value=now + timedelta(days=380)):
|
with patch("zerver.lib.compatibility.timezone_now", return_value=now + timedelta(days=380)):
|
||||||
self.assertEqual(is_outdated_server(iago), True)
|
self.assertEqual(is_outdated_server(iago), True)
|
||||||
self.assertEqual(is_outdated_server(hamlet), False)
|
self.assertEqual(is_outdated_server(hamlet), False)
|
||||||
self.assertEqual(is_outdated_server(None), False)
|
self.assertEqual(is_outdated_server(None), False)
|
||||||
|
|||||||
@@ -1,73 +1,9 @@
|
|||||||
import re
|
|
||||||
from typing import List, Optional, Tuple
|
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from version import DESKTOP_MINIMUM_VERSION, DESKTOP_WARNING_VERSION
|
from zerver.lib.compatibility import find_mobile_os, version_lt
|
||||||
from zerver.lib.response import json_error, json_success
|
from zerver.lib.response import json_error, json_success
|
||||||
from zerver.lib.user_agent import parse_user_agent
|
from zerver.lib.user_agent import parse_user_agent
|
||||||
from zerver.signals import get_device_browser
|
|
||||||
|
|
||||||
|
|
||||||
def pop_numerals(ver: str) -> Tuple[List[int], str]:
|
|
||||||
match = re.search(r"^( \d+ (?: \. \d+ )* ) (.*)", ver, re.X)
|
|
||||||
if match is None:
|
|
||||||
return [], ver
|
|
||||||
numerals, rest = match.groups()
|
|
||||||
numbers = [int(n) for n in numerals.split(".")]
|
|
||||||
return numbers, rest
|
|
||||||
|
|
||||||
|
|
||||||
def version_lt(ver1: str, ver2: str) -> Optional[bool]:
|
|
||||||
"""
|
|
||||||
Compare two Zulip-style version strings.
|
|
||||||
|
|
||||||
Versions are dot-separated sequences of decimal integers,
|
|
||||||
followed by arbitrary trailing decoration. Comparison is
|
|
||||||
lexicographic on the integer sequences, and refuses to
|
|
||||||
guess how any trailing decoration compares to any other,
|
|
||||||
to further numerals, or to nothing.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if ver1 < ver2
|
|
||||||
False if ver1 >= ver2
|
|
||||||
None if can't tell.
|
|
||||||
"""
|
|
||||||
num1, rest1 = pop_numerals(ver1)
|
|
||||||
num2, rest2 = pop_numerals(ver2)
|
|
||||||
if not num1 or not num2:
|
|
||||||
return None
|
|
||||||
common_len = min(len(num1), len(num2))
|
|
||||||
common_num1, rest_num1 = num1[:common_len], num1[common_len:]
|
|
||||||
common_num2, rest_num2 = num2[:common_len], num2[common_len:]
|
|
||||||
|
|
||||||
# Leading numbers win.
|
|
||||||
if common_num1 != common_num2:
|
|
||||||
return common_num1 < common_num2
|
|
||||||
|
|
||||||
# More numbers beats end-of-string, but ??? vs trailing text.
|
|
||||||
# (NB at most one of rest_num1, rest_num2 is nonempty.)
|
|
||||||
if not rest1 and rest_num2:
|
|
||||||
return True
|
|
||||||
if rest_num1 and not rest2:
|
|
||||||
return False
|
|
||||||
if rest_num1 or rest_num2:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Trailing text we can only compare for equality.
|
|
||||||
if rest1 == rest2:
|
|
||||||
return False
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def find_mobile_os(user_agent: str) -> Optional[str]:
|
|
||||||
if re.search(r"\b Android \b", user_agent, re.I | re.X):
|
|
||||||
return "android"
|
|
||||||
if re.search(r"\b(?: iOS | iPhone\ OS )\b", user_agent, re.I | re.X):
|
|
||||||
return "ios"
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# Zulip Mobile release 16.2.96 was made 2018-08-22. It fixed a
|
# Zulip Mobile release 16.2.96 was made 2018-08-22. It fixed a
|
||||||
# bug in our Android code that causes spammy, obviously-broken
|
# bug in our Android code that causes spammy, obviously-broken
|
||||||
@@ -91,38 +27,3 @@ def check_global_compatibility(request: HttpRequest) -> HttpResponse:
|
|||||||
if user_os == "android" and version_lt(user_agent["version"], android_min_app_version):
|
if user_os == "android" and version_lt(user_agent["version"], android_min_app_version):
|
||||||
return json_error(legacy_compatibility_error_message)
|
return json_error(legacy_compatibility_error_message)
|
||||||
return json_success()
|
return json_success()
|
||||||
|
|
||||||
|
|
||||||
def is_outdated_desktop_app(user_agent_str: str) -> Tuple[bool, bool, bool]:
|
|
||||||
# Returns (insecure, banned, auto_update_broken)
|
|
||||||
user_agent = parse_user_agent(user_agent_str)
|
|
||||||
if user_agent["name"] == "ZulipDesktop":
|
|
||||||
# The deprecated QT/webkit based desktop app, last updated in ~2016.
|
|
||||||
return (True, True, True)
|
|
||||||
|
|
||||||
if user_agent["name"] != "ZulipElectron":
|
|
||||||
return (False, False, False)
|
|
||||||
|
|
||||||
if version_lt(user_agent["version"], "4.0.0"):
|
|
||||||
# Version 2.3.82 and older (aka <4.0.0) of the modern
|
|
||||||
# Electron-based Zulip desktop app with known security issues.
|
|
||||||
# won't auto-update; we may want a special notice to
|
|
||||||
# distinguish those from modern releases.
|
|
||||||
return (True, True, True)
|
|
||||||
|
|
||||||
if version_lt(user_agent["version"], DESKTOP_MINIMUM_VERSION):
|
|
||||||
# Below DESKTOP_MINIMUM_VERSION, we reject access as well.
|
|
||||||
return (True, True, False)
|
|
||||||
|
|
||||||
if version_lt(user_agent["version"], DESKTOP_WARNING_VERSION):
|
|
||||||
# Other insecure versions should just warn.
|
|
||||||
return (True, False, False)
|
|
||||||
|
|
||||||
return (False, False, False)
|
|
||||||
|
|
||||||
|
|
||||||
def is_unsupported_browser(user_agent: str) -> Tuple[bool, Optional[str]]:
|
|
||||||
browser_name = get_device_browser(user_agent)
|
|
||||||
if browser_name == "Internet Explorer":
|
|
||||||
return (True, browser_name)
|
|
||||||
return (False, browser_name)
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from zerver.context_processors import get_valid_realm_from_request
|
|||||||
from zerver.decorator import web_public_view, zulip_login_required
|
from zerver.decorator import web_public_view, zulip_login_required
|
||||||
from zerver.forms import ToSForm
|
from zerver.forms import ToSForm
|
||||||
from zerver.lib.actions import do_change_tos_version, realm_user_count
|
from zerver.lib.actions import do_change_tos_version, realm_user_count
|
||||||
|
from zerver.lib.compatibility import is_outdated_desktop_app, is_unsupported_browser
|
||||||
from zerver.lib.home import (
|
from zerver.lib.home import (
|
||||||
build_page_params_for_home_page_load,
|
build_page_params_for_home_page_load,
|
||||||
get_billing_info,
|
get_billing_info,
|
||||||
@@ -24,7 +25,6 @@ from zerver.lib.subdomains import get_subdomain
|
|||||||
from zerver.lib.users import compute_show_invites_and_add_streams
|
from zerver.lib.users import compute_show_invites_and_add_streams
|
||||||
from zerver.lib.utils import statsd
|
from zerver.lib.utils import statsd
|
||||||
from zerver.models import PreregistrationUser, Realm, Stream, UserProfile
|
from zerver.models import PreregistrationUser, Realm, Stream, UserProfile
|
||||||
from zerver.views.compatibility import is_outdated_desktop_app, is_unsupported_browser
|
|
||||||
from zerver.views.portico import hello_view
|
from zerver.views.portico import hello_view
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user