From 45bb8d25804eacd891f33a072171b47405670dda Mon Sep 17 00:00:00 2001 From: Anders Kaseorg Date: Thu, 14 Dec 2023 16:16:00 -0800 Subject: [PATCH] models: Extract zerver.models.users. Signed-off-by: Anders Kaseorg --- analytics/tests/test_counts.py | 3 +- analytics/views/support.py | 2 +- analytics/views/user_activity.py | 3 +- corporate/lib/registration.py | 3 +- corporate/lib/stripe.py | 10 +- corporate/lib/stripe_event_handler.py | 2 +- corporate/tests/test_stripe.py | 11 +- docs/production/authentication-methods.md | 2 +- docs/subsystems/typing-indicators.md | 2 +- .../check_send_receive_time | 3 +- tools/generate-integration-docs-screenshot | 3 +- tools/test-api | 3 +- web/tests/bot_data.test.js | 2 +- zerver/actions/bots.py | 3 +- zerver/actions/create_realm.py | 2 +- zerver/actions/create_user.py | 4 +- zerver/actions/custom_profile_fields.py | 2 +- zerver/actions/default_streams.py | 2 +- zerver/actions/message_edit.py | 2 +- zerver/actions/message_send.py | 4 +- zerver/actions/presence.py | 3 +- zerver/actions/realm_domains.py | 2 +- zerver/actions/realm_emoji.py | 2 +- zerver/actions/realm_icon.py | 3 +- zerver/actions/realm_linkifiers.py | 2 +- zerver/actions/realm_logo.py | 3 +- zerver/actions/realm_playgrounds.py | 10 +- zerver/actions/realm_settings.py | 2 +- zerver/actions/scheduled_messages.py | 2 +- zerver/actions/streams.py | 4 +- zerver/actions/typing.py | 3 +- zerver/actions/user_groups.py | 2 +- zerver/actions/user_settings.py | 3 +- zerver/actions/users.py | 6 +- zerver/decorator.py | 3 +- zerver/forms.py | 3 +- zerver/lib/addressee.py | 6 +- zerver/lib/bot_lib.py | 3 +- zerver/lib/cache.py | 4 +- zerver/lib/email_mirror.py | 3 +- zerver/lib/email_notifications.py | 2 +- zerver/lib/email_validation.py | 3 +- zerver/lib/events.py | 2 +- zerver/lib/export.py | 3 +- zerver/lib/import_realm.py | 3 +- zerver/lib/narrow.py | 2 + zerver/lib/onboarding.py | 3 +- zerver/lib/outgoing_webhook.py | 2 +- zerver/lib/push_notifications.py | 2 +- zerver/lib/recipient_users.py | 3 +- zerver/lib/send_email.py | 3 +- zerver/lib/server_initialization.py | 2 +- zerver/lib/sessions.py | 3 +- zerver/lib/streams.py | 4 +- zerver/lib/test_classes.py | 4 +- zerver/lib/upload/base.py | 3 +- zerver/lib/users.py | 4 +- zerver/management/commands/check_redis.py | 2 +- zerver/management/commands/export_search.py | 10 +- zerver/management/commands/rate_limit.py | 3 +- zerver/models/__init__.py | 1057 +--------------- zerver/models/users.py | 1077 +++++++++++++++++ zerver/openapi/curl_param_value_generators.py | 3 +- zerver/openapi/python_examples.py | 3 +- zerver/tests/test_auth_backends.py | 3 +- zerver/tests/test_bots.py | 3 +- zerver/tests/test_cache.py | 3 +- zerver/tests/test_decorators.py | 3 +- zerver/tests/test_email_change.py | 10 +- zerver/tests/test_email_mirror.py | 11 +- zerver/tests/test_embedded_bot_system.py | 9 +- zerver/tests/test_event_system.py | 2 +- zerver/tests/test_events.py | 2 +- zerver/tests/test_example.py | 3 +- zerver/tests/test_home.py | 3 +- zerver/tests/test_import_export.py | 3 +- zerver/tests/test_integrations_dev_panel.py | 3 +- zerver/tests/test_invite.py | 2 +- zerver/tests/test_management_commands.py | 12 +- zerver/tests/test_mattermost_importer.py | 3 +- zerver/tests/test_message_send.py | 3 +- zerver/tests/test_mirror_users.py | 3 +- .../tests/test_outgoing_webhook_interfaces.py | 10 +- zerver/tests/test_realm.py | 3 +- zerver/tests/test_retention.py | 2 +- zerver/tests/test_rocketchat_importer.py | 3 +- zerver/tests/test_settings.py | 2 +- zerver/tests/test_signup.py | 4 +- zerver/tests/test_subs.py | 4 +- zerver/tests/test_tutorial.py | 3 +- zerver/tests/test_upload.py | 3 +- zerver/tests/test_upload_local.py | 3 +- zerver/tests/test_upload_s3.py | 3 +- zerver/tests/test_users.py | 4 +- zerver/tests/test_webhooks_common.py | 3 +- zerver/tests/test_zephyr.py | 3 +- zerver/tornado/views.py | 3 +- zerver/views/auth.py | 2 +- zerver/views/development/email_log.py | 3 +- zerver/views/message_send.py | 3 +- zerver/views/presence.py | 10 +- zerver/views/registration.py | 3 +- zerver/views/streams.py | 3 +- zerver/views/user_groups.py | 3 +- zerver/views/users.py | 2 + zerver/webhooks/dialogflow/view.py | 3 +- zerver/webhooks/helloworld/tests.py | 3 +- zerver/webhooks/jira/view.py | 3 +- zerver/webhooks/teamcity/tests.py | 3 +- zerver/worker/queue_processors.py | 3 +- zilencer/management/commands/populate_db.py | 4 +- zilencer/management/commands/sync_api_key.py | 3 +- zproject/backends.py | 6 +- zproject/sentry.py | 2 +- 114 files changed, 1260 insertions(+), 1273 deletions(-) create mode 100644 zerver/models/users.py diff --git a/analytics/tests/test_counts.py b/analytics/tests/test_counts.py index b3e514c660..b3af4ac8d5 100644 --- a/analytics/tests/test_counts.py +++ b/analytics/tests/test_counts.py @@ -79,9 +79,8 @@ from zerver.models import ( UserGroup, UserProfile, get_client, - get_user, - is_cross_realm_bot_email, ) +from zerver.models.users import get_user, is_cross_realm_bot_email from zilencer.models import ( RemoteInstallationCount, RemotePushDeviceToken, diff --git a/analytics/views/support.py b/analytics/views/support.py index b9e91cfc05..06ee893051 100644 --- a/analytics/views/support.py +++ b/analytics/views/support.py @@ -42,8 +42,8 @@ from zerver.models import ( UserProfile, get_org_type_display_name, get_realm, - get_user_profile_by_id, ) +from zerver.models.users import get_user_profile_by_id from zerver.views.invite import get_invitee_emails_set if settings.ZILENCER_ENABLED: diff --git a/analytics/views/user_activity.py b/analytics/views/user_activity.py index 5143f58c8f..1b1778efb3 100644 --- a/analytics/views/user_activity.py +++ b/analytics/views/user_activity.py @@ -11,7 +11,8 @@ from analytics.views.activity_common import ( make_table, ) from zerver.decorator import require_server_admin -from zerver.models import UserActivity, UserProfile, get_user_profile_by_id +from zerver.models import UserActivity, UserProfile +from zerver.models.users import get_user_profile_by_id if settings.BILLING_ENABLED: pass diff --git a/corporate/lib/registration.py b/corporate/lib/registration.py index 65d50b5d5e..3bfc9c6a78 100644 --- a/corporate/lib/registration.py +++ b/corporate/lib/registration.py @@ -7,7 +7,8 @@ from corporate.lib.stripe import LicenseLimitError, get_latest_seat_count, get_s from corporate.models import get_current_plan_by_realm from zerver.actions.create_user import send_message_to_signup_notification_stream from zerver.lib.exceptions import InvitationError -from zerver.models import Realm, UserProfile, get_system_bot +from zerver.models import Realm, UserProfile +from zerver.models.users import get_system_bot def generate_licenses_low_warning_message_if_required(realm: Realm) -> Optional[str]: diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 4fb4398d4d..df861af806 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -50,14 +50,8 @@ from zerver.lib.send_email import ( from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime from zerver.lib.url_encoding import append_url_query_string from zerver.lib.utils import assert_is_not_none -from zerver.models import ( - Realm, - RealmAuditLog, - UserProfile, - get_org_type_display_name, - get_realm, - get_system_bot, -) +from zerver.models import Realm, RealmAuditLog, UserProfile, get_org_type_display_name, get_realm +from zerver.models.users import get_system_bot from zilencer.lib.remote_counts import MissingDataError from zilencer.models import ( RemoteRealm, diff --git a/corporate/lib/stripe_event_handler.py b/corporate/lib/stripe_event_handler.py index da90cbe849..1cd7d00521 100644 --- a/corporate/lib/stripe_event_handler.py +++ b/corporate/lib/stripe_event_handler.py @@ -13,7 +13,7 @@ from corporate.lib.stripe import ( UpgradeWithExistingPlanError, ) from corporate.models import Customer, CustomerPlan, Event, PaymentIntent, Session -from zerver.models import get_active_user_profile_by_id_in_realm +from zerver.models.users import get_active_user_profile_by_id_in_realm billing_logger = logging.getLogger("corporate.stripe") diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index ddb43c4b7d..4653c3594f 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -102,15 +102,8 @@ from zerver.lib.remote_server import send_server_data_to_push_bouncer from zerver.lib.test_classes import ZulipTestCase from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime from zerver.lib.utils import assert_is_not_none -from zerver.models import ( - Message, - Realm, - RealmAuditLog, - Recipient, - UserProfile, - get_realm, - get_system_bot, -) +from zerver.models import Message, Realm, RealmAuditLog, Recipient, UserProfile, get_realm +from zerver.models.users import get_system_bot from zilencer.lib.remote_counts import MissingDataError from zilencer.models import ( RemoteRealm, diff --git a/docs/production/authentication-methods.md b/docs/production/authentication-methods.md index 7247c93c91..2e2262ffbf 100644 --- a/docs/production/authentication-methods.md +++ b/docs/production/authentication-methods.md @@ -365,7 +365,7 @@ You can look at the [full list of fields][models-py] in the Zulip user model; search for `class UserProfile`, but the above should cover all the fields that would be useful to sync from your LDAP databases. -[models-py]: https://github.com/zulip/zulip/blob/main/zerver/models/__init__.py +[models-py]: https://github.com/zulip/zulip/blob/main/zerver/models/users.py [django-auth-booleans]: https://django-auth-ldap.readthedocs.io/en/latest/users.html#easy-attributes ### Multiple LDAP searches diff --git a/docs/subsystems/typing-indicators.md b/docs/subsystems/typing-indicators.md index 6ecf0468d3..b69c5570e4 100644 --- a/docs/subsystems/typing-indicators.md +++ b/docs/subsystems/typing-indicators.md @@ -37,7 +37,7 @@ On a high level the typing indicators system works like this: Note that there is a user-level privacy setting to disable sending typing notifications that a client should check when implementing the "writing user" protocol below. See `send_private_typing_notifications` -in the `UserBaseSettings` model in `zerver/models/__init__.py` and in the +in the `UserBaseSettings` model in `zerver/models/users.py` and in the `user_settings` object in the `POST /register` response. ## Writing user diff --git a/puppet/zulip/files/nagios_plugins/zulip_app_frontend/check_send_receive_time b/puppet/zulip/files/nagios_plugins/zulip_app_frontend/check_send_receive_time index 4907ad217b..bcf3842ef9 100755 --- a/puppet/zulip/files/nagios_plugins/zulip_app_frontend/check_send_receive_time +++ b/puppet/zulip/files/nagios_plugins/zulip_app_frontend/check_send_receive_time @@ -66,7 +66,8 @@ django.setup() from django.conf import settings -from zerver.models import get_realm, get_system_bot +from zerver.models import get_realm +from zerver.models.users import get_system_bot states = { "OK": 0, diff --git a/tools/generate-integration-docs-screenshot b/tools/generate-integration-docs-screenshot index b8678eda44..e00d5fb30e 100755 --- a/tools/generate-integration-docs-screenshot +++ b/tools/generate-integration-docs-screenshot @@ -50,7 +50,8 @@ from zerver.lib.storage import static_path from zerver.lib.streams import create_stream_if_needed from zerver.lib.upload import upload_avatar_image from zerver.lib.webhooks.common import get_fixture_http_headers -from zerver.models import Message, UserProfile, get_realm, get_user_by_delivery_email +from zerver.models import Message, UserProfile, get_realm +from zerver.models.users import get_user_by_delivery_email def create_integration_bot(integration: Integration, bot_name: Optional[str] = None) -> UserProfile: diff --git a/tools/test-api b/tools/test-api index ff3d34ff0c..8a371641e9 100755 --- a/tools/test-api +++ b/tools/test-api @@ -36,7 +36,8 @@ with test_server_running( from zerver.actions.users import change_user_is_active from zerver.lib.test_helpers import reset_email_visibility_to_everyone_in_zulip_realm from zerver.lib.users import get_api_key - from zerver.models import get_realm, get_user + from zerver.models import get_realm + from zerver.models.users import get_user from zerver.openapi.javascript_examples import test_js_bindings from zerver.openapi.python_examples import ( test_invalid_api_key, diff --git a/web/tests/bot_data.test.js b/web/tests/bot_data.test.js index c1ac83e95e..f4bd9946dd 100644 --- a/web/tests/bot_data.test.js +++ b/web/tests/bot_data.test.js @@ -10,7 +10,7 @@ const bot_data = zrequire("bot_data"); const people = zrequire("people"); // Bot types and service bot types can be found -// in zerver/models/__init__.py - UserProfile Class or +// in zerver/models/users.py - UserProfile Class or // zever/openapi/zulip.yaml const me = { diff --git a/zerver/actions/bots.py b/zerver/actions/bots.py index c790d2b690..5c30ee50a2 100644 --- a/zerver/actions/bots.py +++ b/zerver/actions/bots.py @@ -6,7 +6,8 @@ from django.utils.timezone import now as timezone_now from zerver.actions.create_user import created_bot_event from zerver.actions.streams import bulk_remove_subscriptions from zerver.lib.streams import get_subscribed_private_streams_for_user -from zerver.models import RealmAuditLog, Stream, UserProfile, active_user_ids, bot_owner_user_ids +from zerver.models import RealmAuditLog, Stream, UserProfile +from zerver.models.users import active_user_ids, bot_owner_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/create_realm.py b/zerver/actions/create_realm.py index e23118fb8f..a87b851bd3 100644 --- a/zerver/actions/create_realm.py +++ b/zerver/actions/create_realm.py @@ -33,8 +33,8 @@ from zerver.models import ( UserProfile, get_org_type_display_name, get_realm, - get_system_bot, ) +from zerver.models.users import get_system_bot from zproject.backends import all_implemented_backend_names if settings.CORPORATE_ENABLED: diff --git a/zerver/actions/create_user.py b/zerver/actions/create_user.py index f30f2f5302..f52586ffab 100644 --- a/zerver/actions/create_user.py +++ b/zerver/actions/create_user.py @@ -49,10 +49,8 @@ from zerver.models import ( UserGroupMembership, UserMessage, UserProfile, - active_user_ids, - bot_owner_user_ids, - get_system_bot, ) +from zerver.models.users import active_user_ids, bot_owner_user_ids, get_system_bot from zerver.tornado.django_api import send_event_on_commit if settings.BILLING_ENABLED: diff --git a/zerver/actions/custom_profile_fields.py b/zerver/actions/custom_profile_fields.py index 8f2c60fdf3..c01ace2ba0 100644 --- a/zerver/actions/custom_profile_fields.py +++ b/zerver/actions/custom_profile_fields.py @@ -14,9 +14,9 @@ from zerver.models import ( CustomProfileFieldValue, Realm, UserProfile, - active_user_ids, custom_profile_fields_for_realm, ) +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event diff --git a/zerver/actions/default_streams.py b/zerver/actions/default_streams.py index 467d278648..f298b2fc99 100644 --- a/zerver/actions/default_streams.py +++ b/zerver/actions/default_streams.py @@ -13,9 +13,9 @@ from zerver.models import ( DefaultStreamGroup, Realm, Stream, - active_non_guest_user_ids, get_default_stream_groups, ) +from zerver.models.users import active_non_guest_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/message_edit.py b/zerver/actions/message_edit.py index d542de77bf..c9014ed0ef 100644 --- a/zerver/actions/message_edit.py +++ b/zerver/actions/message_edit.py @@ -77,8 +77,8 @@ from zerver.models import ( UserProfile, UserTopic, get_stream_by_id_in_realm, - get_system_bot, ) +from zerver.models.users import get_system_bot from zerver.tornado.django_api import send_event diff --git a/zerver/actions/message_send.py b/zerver/actions/message_send.py index 6a3ccb08a0..11ded34975 100644 --- a/zerver/actions/message_send.py +++ b/zerver/actions/message_send.py @@ -107,11 +107,9 @@ from zerver.models import ( get_huddle_user_ids, get_stream, get_stream_by_id_in_realm, - get_system_bot, - get_user_by_delivery_email, - is_cross_realm_bot_email, query_for_ids, ) +from zerver.models.users import get_system_bot, get_user_by_delivery_email, is_cross_realm_bot_email from zerver.tornado.django_api import send_event diff --git a/zerver/actions/presence.py b/zerver/actions/presence.py index 25031bec6c..83b720e72b 100644 --- a/zerver/actions/presence.py +++ b/zerver/actions/presence.py @@ -12,7 +12,8 @@ from zerver.lib.presence import ( from zerver.lib.queue import queue_json_publish from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.users import get_user_ids_who_can_access_user -from zerver.models import Client, UserPresence, UserProfile, active_user_ids, get_client +from zerver.models import Client, UserPresence, UserProfile, get_client +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event diff --git a/zerver/actions/realm_domains.py b/zerver/actions/realm_domains.py index 57526dfab7..ed03e9fd0b 100644 --- a/zerver/actions/realm_domains.py +++ b/zerver/actions/realm_domains.py @@ -10,9 +10,9 @@ from zerver.models import ( RealmDomain, RealmDomainDict, UserProfile, - active_user_ids, get_realm_domains, ) +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/realm_emoji.py b/zerver/actions/realm_emoji.py index 86fbc83f11..318a6ac45c 100644 --- a/zerver/actions/realm_emoji.py +++ b/zerver/actions/realm_emoji.py @@ -16,9 +16,9 @@ from zerver.models import ( RealmAuditLog, RealmEmoji, UserProfile, - active_user_ids, get_all_custom_emoji_for_realm, ) +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/realm_icon.py b/zerver/actions/realm_icon.py index df0f949fd9..820eeee36c 100644 --- a/zerver/actions/realm_icon.py +++ b/zerver/actions/realm_icon.py @@ -4,7 +4,8 @@ from django.db import transaction from django.utils.timezone import now as timezone_now from zerver.lib.realm_icon import realm_icon_url -from zerver.models import Realm, RealmAuditLog, UserProfile, active_user_ids +from zerver.models import Realm, RealmAuditLog, UserProfile +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/realm_linkifiers.py b/zerver/actions/realm_linkifiers.py index 4d7d1a22c7..a7f1e4315f 100644 --- a/zerver/actions/realm_linkifiers.py +++ b/zerver/actions/realm_linkifiers.py @@ -12,10 +12,10 @@ from zerver.models import ( RealmAuditLog, RealmFilter, UserProfile, - active_user_ids, flush_linkifiers, linkifiers_for_realm, ) +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/realm_logo.py b/zerver/actions/realm_logo.py index ec0b7451d4..687c530062 100644 --- a/zerver/actions/realm_logo.py +++ b/zerver/actions/realm_logo.py @@ -4,7 +4,8 @@ from django.db import transaction from django.utils.timezone import now as timezone_now from zerver.lib.realm_logo import get_realm_logo_data -from zerver.models import Realm, RealmAuditLog, UserProfile, active_user_ids +from zerver.models import Realm, RealmAuditLog, UserProfile +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/realm_playgrounds.py b/zerver/actions/realm_playgrounds.py index 6fe4788c5b..a8adb974c8 100644 --- a/zerver/actions/realm_playgrounds.py +++ b/zerver/actions/realm_playgrounds.py @@ -6,14 +6,8 @@ from django.utils.timezone import now as timezone_now from zerver.lib.exceptions import ValidationFailureError from zerver.lib.types import RealmPlaygroundDict -from zerver.models import ( - Realm, - RealmAuditLog, - RealmPlayground, - UserProfile, - active_user_ids, - get_realm_playgrounds, -) +from zerver.models import Realm, RealmAuditLog, RealmPlayground, UserProfile, get_realm_playgrounds +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event_on_commit diff --git a/zerver/actions/realm_settings.py b/zerver/actions/realm_settings.py index bc974401f0..6bf36fba0a 100644 --- a/zerver/actions/realm_settings.py +++ b/zerver/actions/realm_settings.py @@ -34,9 +34,9 @@ from zerver.models import ( SystemGroups, UserGroup, UserProfile, - active_user_ids, get_realm, ) +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event, send_event_on_commit if settings.BILLING_ENABLED: diff --git a/zerver/actions/scheduled_messages.py b/zerver/actions/scheduled_messages.py index bd27335178..8b824041fe 100644 --- a/zerver/actions/scheduled_messages.py +++ b/zerver/actions/scheduled_messages.py @@ -27,8 +27,8 @@ from zerver.models import ( Subscription, UserProfile, get_recipient_ids, - get_system_bot, ) +from zerver.models.users import get_system_bot from zerver.tornado.django_api import send_event SCHEDULED_MESSAGE_LATE_CUTOFF_MINUTES = 10 diff --git a/zerver/actions/streams.py b/zerver/actions/streams.py index 065aaee36d..a1bcbeb481 100644 --- a/zerver/actions/streams.py +++ b/zerver/actions/streams.py @@ -67,10 +67,8 @@ from zerver.models import ( SystemGroups, UserGroup, UserProfile, - active_non_guest_user_ids, - active_user_ids, - get_system_bot, ) +from zerver.models.users import active_non_guest_user_ids, active_user_ids, get_system_bot from zerver.tornado.django_api import send_event, send_event_on_commit diff --git a/zerver/actions/typing.py b/zerver/actions/typing.py index a2fcbaf5e2..79753d6847 100644 --- a/zerver/actions/typing.py +++ b/zerver/actions/typing.py @@ -5,7 +5,8 @@ from django.utils.translation import gettext as _ from zerver.lib.exceptions import JsonableError from zerver.lib.stream_subscription import get_active_subscriptions_for_stream_id -from zerver.models import Realm, Stream, UserProfile, get_user_by_id_in_realm_including_cross_realm +from zerver.models import Realm, Stream, UserProfile +from zerver.models.users import get_user_by_id_in_realm_including_cross_realm from zerver.tornado.django_api import send_event diff --git a/zerver/actions/user_groups.py b/zerver/actions/user_groups.py index d90f4b5983..aa88270372 100644 --- a/zerver/actions/user_groups.py +++ b/zerver/actions/user_groups.py @@ -19,8 +19,8 @@ from zerver.models import ( UserGroup, UserGroupMembership, UserProfile, - active_user_ids, ) +from zerver.models.users import active_user_ids from zerver.tornado.django_api import send_event, send_event_on_commit diff --git a/zerver/actions/user_settings.py b/zerver/actions/user_settings.py index e73f014fa7..a8127ec684 100644 --- a/zerver/actions/user_settings.py +++ b/zerver/actions/user_settings.py @@ -39,10 +39,9 @@ from zerver.models import ( ScheduledMessageNotificationEmail, UserPresence, UserProfile, - bot_owner_user_ids, get_client, - get_user_profile_by_id, ) +from zerver.models.users import bot_owner_user_ids, get_user_profile_by_id from zerver.tornado.django_api import send_event, send_event_on_commit diff --git a/zerver/actions/users.py b/zerver/actions/users.py index 187238c095..bda9c80aa9 100644 --- a/zerver/actions/users.py +++ b/zerver/actions/users.py @@ -41,12 +41,14 @@ from zerver.models import ( Subscription, UserGroupMembership, UserProfile, + get_bot_services, + get_fake_email_domain, +) +from zerver.models.users import ( active_non_guest_user_ids, active_user_ids, bot_owner_user_ids, get_bot_dicts_in_realm, - get_bot_services, - get_fake_email_domain, get_user_profile_by_id, ) from zerver.tornado.django_api import send_event, send_event_on_commit diff --git a/zerver/decorator.py b/zerver/decorator.py index fe20058b7b..aef1b84afb 100644 --- a/zerver/decorator.py +++ b/zerver/decorator.py @@ -61,7 +61,8 @@ from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime from zerver.lib.users import is_2fa_verified from zerver.lib.utils import has_api_key_format from zerver.lib.webhooks.common import notify_bot_owner_about_invalid_json -from zerver.models import UserProfile, get_client, get_user_profile_by_api_key +from zerver.models import UserProfile, get_client +from zerver.models.users import get_user_profile_by_api_key if TYPE_CHECKING: from django.http.request import _ImmutableQueryDict diff --git a/zerver/forms.py b/zerver/forms.py index 467983e29f..ac53d952ef 100644 --- a/zerver/forms.py +++ b/zerver/forms.py @@ -42,9 +42,8 @@ from zerver.models import ( Realm, UserProfile, get_realm, - get_user_by_delivery_email, - is_cross_realm_bot_email, ) +from zerver.models.users import get_user_by_delivery_email, is_cross_realm_bot_email from zproject.backends import check_password_strength, email_auth_enabled, email_belongs_to_ldap if settings.BILLING_ENABLED: diff --git a/zerver/lib/addressee.py b/zerver/lib/addressee.py index 06a786eff8..24fff0cf9c 100644 --- a/zerver/lib/addressee.py +++ b/zerver/lib/addressee.py @@ -4,10 +4,8 @@ from django.utils.translation import gettext as _ from zerver.lib.exceptions import JsonableError from zerver.lib.string_validation import check_stream_topic -from zerver.models import ( - Realm, - Stream, - UserProfile, +from zerver.models import Realm, Stream, UserProfile +from zerver.models.users import ( get_user_by_id_in_realm_including_cross_realm, get_user_including_cross_realm, ) diff --git a/zerver/lib/bot_lib.py b/zerver/lib/bot_lib.py index 4355d80679..ae7cebf0e1 100644 --- a/zerver/lib/bot_lib.py +++ b/zerver/lib/bot_lib.py @@ -20,7 +20,8 @@ from zerver.lib.bot_storage import ( ) from zerver.lib.integrations import EMBEDDED_BOTS from zerver.lib.topic import get_topic_from_message_info -from zerver.models import UserProfile, get_active_user +from zerver.models import UserProfile +from zerver.models.users import get_active_user def get_bot_handler(service_name: str) -> Any: diff --git a/zerver/lib/cache.py b/zerver/lib/cache.py index 87da24e641..6725ca9504 100644 --- a/zerver/lib/cache.py +++ b/zerver/lib/cache.py @@ -518,7 +518,7 @@ def bot_dicts_in_realm_cache_key(realm_id: int) -> str: def delete_user_profile_caches(user_profiles: Iterable["UserProfile"], realm: "Realm") -> None: # Imported here to avoid cyclic dependency. from zerver.lib.users import get_all_api_keys - from zerver.models import is_cross_realm_bot_email + from zerver.models.users import is_cross_realm_bot_email keys = [] for user_profile in user_profiles: @@ -554,7 +554,7 @@ def changed(update_fields: Optional[Sequence[str]], fields: List[str]) -> bool: return any(f in update_fields_set for f in fields) -# Called by models/__init__.py to flush the user_profile cache whenever we save +# Called by models/users.py to flush the user_profile cache whenever we save # a user_profile object def flush_user_profile( *, diff --git a/zerver/lib/email_mirror.py b/zerver/lib/email_mirror.py index 7b02c86e19..06115f0581 100644 --- a/zerver/lib/email_mirror.py +++ b/zerver/lib/email_mirror.py @@ -38,9 +38,8 @@ from zerver.models import ( get_client, get_display_recipient, get_stream_by_id_in_realm, - get_system_bot, - get_user_profile_by_id, ) +from zerver.models.users import get_system_bot, get_user_profile_by_id from zproject.backends import is_user_active logger = logging.getLogger(__name__) diff --git a/zerver/lib/email_notifications.py b/zerver/lib/email_notifications.py index 11e59340eb..afbddc26a2 100644 --- a/zerver/lib/email_notifications.py +++ b/zerver/lib/email_notifications.py @@ -45,8 +45,8 @@ from zerver.models import ( UserProfile, get_context_for_message, get_display_recipient, - get_user_profile_by_id, ) +from zerver.models.users import get_user_profile_by_id if sys.version_info < (3, 9): # nocoverage from backports import zoneinfo diff --git a/zerver/lib/email_validation.py b/zerver/lib/email_validation.py index 0f37829d61..be7416cdcf 100644 --- a/zerver/lib/email_validation.py +++ b/zerver/lib/email_validation.py @@ -14,9 +14,8 @@ from zerver.models import ( EmailContainsPlusError, Realm, RealmDomain, - get_users_by_delivery_email, - is_cross_realm_bot_email, ) +from zerver.models.users import get_users_by_delivery_email, is_cross_realm_bot_email def validate_disposable(email: str) -> None: diff --git a/zerver/lib/events.py b/zerver/lib/events.py index 95ee9af6d2..bd50e91064 100644 --- a/zerver/lib/events.py +++ b/zerver/lib/events.py @@ -453,7 +453,7 @@ def fetch_initial_state_data( assert spectator_requested_language is not None # When UserProfile=None, we want to serve the values for various # settings as the defaults. Instead of copying the default values - # from models/__init__.py here, we access these default values from a + # from models/users.py here, we access these default values from a # temporary UserProfile object that will not be saved to the database. # # We also can set various fields to avoid duplicating code diff --git a/zerver/lib/export.py b/zerver/lib/export.py index 9549b5f42b..46dffacf27 100644 --- a/zerver/lib/export.py +++ b/zerver/lib/export.py @@ -70,9 +70,8 @@ from zerver.models import ( UserStatus, UserTopic, get_realm, - get_system_bot, - get_user_profile_by_id, ) +from zerver.models.users import get_system_bot, get_user_profile_by_id # Custom mypy types follow: Record: TypeAlias = Dict[str, Any] diff --git a/zerver/lib/import_realm.py b/zerver/lib/import_realm.py index 7ceaf49bcb..8677e73a35 100644 --- a/zerver/lib/import_realm.py +++ b/zerver/lib/import_realm.py @@ -79,9 +79,8 @@ from zerver.models import ( UserTopic, get_huddle_hash, get_realm, - get_system_bot, - get_user_profile_by_id, ) +from zerver.models.users import get_system_bot, get_user_profile_by_id realm_tables = [ ("zerver_realmauthenticationmethod", RealmAuthenticationMethod, "realmauthenticationmethod"), diff --git a/zerver/lib/narrow.py b/zerver/lib/narrow.py index bcb7656900..e5e47e11a9 100644 --- a/zerver/lib/narrow.py +++ b/zerver/lib/narrow.py @@ -83,6 +83,8 @@ from zerver.models import ( UserMessage, UserProfile, get_active_streams, +) +from zerver.models.users import ( get_user_by_id_in_realm_including_cross_realm, get_user_including_cross_realm, ) diff --git a/zerver/lib/onboarding.py b/zerver/lib/onboarding.py index 96c6bfa75e..4877c8f92f 100644 --- a/zerver/lib/onboarding.py +++ b/zerver/lib/onboarding.py @@ -15,7 +15,8 @@ from zerver.actions.message_send import ( from zerver.actions.reactions import do_add_reaction from zerver.lib.emoji import get_emoji_data from zerver.lib.message import SendMessageRequest -from zerver.models import Message, Realm, UserProfile, get_system_bot +from zerver.models import Message, Realm, UserProfile +from zerver.models.users import get_system_bot def missing_any_realm_internal_bots() -> bool: diff --git a/zerver/lib/outgoing_webhook.py b/zerver/lib/outgoing_webhook.py index cf8f604d7e..dd2a653ac8 100644 --- a/zerver/lib/outgoing_webhook.py +++ b/zerver/lib/outgoing_webhook.py @@ -26,8 +26,8 @@ from zerver.models import ( Service, UserProfile, get_client, - get_user_profile_by_id, ) +from zerver.models.users import get_user_profile_by_id class OutgoingWebhookServiceInterface(metaclass=abc.ABCMeta): diff --git a/zerver/lib/push_notifications.py b/zerver/lib/push_notifications.py index 702363937e..dd66779fb7 100644 --- a/zerver/lib/push_notifications.py +++ b/zerver/lib/push_notifications.py @@ -66,8 +66,8 @@ from zerver.models import ( UserProfile, get_display_recipient, get_fake_email_domain, - get_user_profile_by_id, ) +from zerver.models.users import get_user_profile_by_id if TYPE_CHECKING: import aioapns diff --git a/zerver/lib/recipient_users.py b/zerver/lib/recipient_users.py index 1a9542f3bd..cd4764d886 100644 --- a/zerver/lib/recipient_users.py +++ b/zerver/lib/recipient_users.py @@ -3,7 +3,8 @@ from typing import Dict, Optional, Sequence from django.core.exceptions import ValidationError from django.utils.translation import gettext as _ -from zerver.models import Recipient, UserProfile, get_or_create_huddle, is_cross_realm_bot_email +from zerver.models import Recipient, UserProfile, get_or_create_huddle +from zerver.models.users import is_cross_realm_bot_email def get_recipient_from_user_profiles( diff --git a/zerver/lib/send_email.py b/zerver/lib/send_email.py index 79246c1510..8880bb7f8f 100644 --- a/zerver/lib/send_email.py +++ b/zerver/lib/send_email.py @@ -29,7 +29,8 @@ from django.utils.translation import override as override_language from confirmation.models import generate_key from zerver.lib.logging_util import log_to_file -from zerver.models import EMAIL_TYPES, Realm, ScheduledEmail, UserProfile, get_user_profile_by_id +from zerver.models import EMAIL_TYPES, Realm, ScheduledEmail, UserProfile +from zerver.models.users import get_user_profile_by_id from zproject.email_backends import EmailLogBackEnd, get_forward_address if settings.ZILENCER_ENABLED: diff --git a/zerver/lib/server_initialization.py b/zerver/lib/server_initialization.py index c1e11414c7..33789bd4d0 100644 --- a/zerver/lib/server_initialization.py +++ b/zerver/lib/server_initialization.py @@ -12,8 +12,8 @@ from zerver.models import ( RealmUserDefault, UserProfile, get_client, - get_system_bot, ) +from zerver.models.users import get_system_bot from zproject.backends import all_implemented_backend_names diff --git a/zerver/lib/sessions.py b/zerver/lib/sessions.py index 96c7e3e856..b34636a84a 100644 --- a/zerver/lib/sessions.py +++ b/zerver/lib/sessions.py @@ -10,7 +10,8 @@ from django.contrib.sessions.models import Session from django.utils.timezone import now as timezone_now from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime -from zerver.models import Realm, UserProfile, get_user_profile_by_id +from zerver.models import Realm, UserProfile +from zerver.models.users import get_user_profile_by_id class SessionEngine(Protocol): diff --git a/zerver/lib/streams.py b/zerver/lib/streams.py index 0aaf0d9e24..7036f08a46 100644 --- a/zerver/lib/streams.py +++ b/zerver/lib/streams.py @@ -31,14 +31,12 @@ from zerver.models import ( SystemGroups, UserGroup, UserProfile, - active_non_guest_user_ids, - active_user_ids, bulk_get_streams, get_realm_stream, get_stream, get_stream_by_id_in_realm, - is_cross_realm_bot_email, ) +from zerver.models.users import active_non_guest_user_ids, active_user_ids, is_cross_realm_bot_email from zerver.tornado.django_api import send_event diff --git a/zerver/lib/test_classes.py b/zerver/lib/test_classes.py index 9dc6663f94..217ac8919c 100644 --- a/zerver/lib/test_classes.py +++ b/zerver/lib/test_classes.py @@ -112,10 +112,8 @@ from zerver.models import ( get_realm, get_realm_stream, get_stream, - get_system_bot, - get_user, - get_user_by_delivery_email, ) +from zerver.models.users import get_system_bot, get_user, get_user_by_delivery_email from zerver.openapi.openapi import validate_against_openapi_schema, validate_request from zerver.tornado.event_queue import clear_client_event_queues_for_testing diff --git a/zerver/lib/upload/base.py b/zerver/lib/upload/base.py index 38fd894a00..560a5a5b5e 100644 --- a/zerver/lib/upload/base.py +++ b/zerver/lib/upload/base.py @@ -10,7 +10,8 @@ from PIL import GifImagePlugin, Image, ImageOps, PngImagePlugin from PIL.Image import DecompressionBombError from zerver.lib.exceptions import ErrorCode, JsonableError -from zerver.models import Attachment, Realm, UserProfile, is_cross_realm_bot_email +from zerver.models import Attachment, Realm, UserProfile +from zerver.models.users import is_cross_realm_bot_email DEFAULT_AVATAR_SIZE = 100 MEDIUM_AVATAR_SIZE = 500 diff --git a/zerver/lib/users.py b/zerver/lib/users.py index 4423890920..818f964921 100644 --- a/zerver/lib/users.py +++ b/zerver/lib/users.py @@ -37,9 +37,11 @@ from zerver.models import ( SystemGroups, UserMessage, UserProfile, + get_fake_email_domain, +) +from zerver.models.users import ( active_non_guest_user_ids, active_user_ids, - get_fake_email_domain, get_realm_user_dicts, get_user, get_user_by_id_in_realm_including_cross_realm, diff --git a/zerver/management/commands/check_redis.py b/zerver/management/commands/check_redis.py index 179bbf16b4..b68d5e8492 100644 --- a/zerver/management/commands/check_redis.py +++ b/zerver/management/commands/check_redis.py @@ -8,7 +8,7 @@ from returns.curry import partial from typing_extensions import override from zerver.lib.rate_limiter import RateLimitedUser, client -from zerver.models import get_user_profile_by_id +from zerver.models.users import get_user_profile_by_id class Command(BaseCommand): diff --git a/zerver/management/commands/export_search.py b/zerver/management/commands/export_search.py index c583f6afb0..ceda82d18d 100644 --- a/zerver/management/commands/export_search.py +++ b/zerver/management/commands/export_search.py @@ -16,14 +16,8 @@ from typing_extensions import override from zerver.lib.management import ZulipBaseCommand from zerver.lib.soft_deactivation import reactivate_user_if_soft_deactivated from zerver.lib.upload import save_attachment_contents -from zerver.models import ( - Attachment, - Message, - Recipient, - Stream, - UserProfile, - get_user_by_delivery_email, -) +from zerver.models import Attachment, Message, Recipient, Stream, UserProfile +from zerver.models.users import get_user_by_delivery_email def write_attachment(base_path: str, attachment: Attachment) -> None: diff --git a/zerver/management/commands/rate_limit.py b/zerver/management/commands/rate_limit.py index 98caf8228e..c45de0932a 100644 --- a/zerver/management/commands/rate_limit.py +++ b/zerver/management/commands/rate_limit.py @@ -6,7 +6,8 @@ from typing_extensions import override from zerver.lib.management import ZulipBaseCommand from zerver.lib.rate_limiter import RateLimitedUser -from zerver.models import UserProfile, get_user_profile_by_api_key +from zerver.models import UserProfile +from zerver.models.users import get_user_profile_by_api_key class Command(ZulipBaseCommand): diff --git a/zerver/models/__init__.py b/zerver/models/__init__.py index fd999cf69c..0003ea9514 100644 --- a/zerver/models/__init__.py +++ b/zerver/models/__init__.py @@ -31,12 +31,7 @@ import uri_template from bitfield import BitField from bitfield.types import Bit, BitHandler from django.conf import settings -from django.contrib.auth.models import ( - AbstractBaseUser, - AnonymousUser, - PermissionsMixin, - UserManager, -) +from django.contrib.auth.models import AnonymousUser from django.contrib.contenttypes.fields import GenericRelation from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.search import SearchVectorField @@ -59,11 +54,6 @@ from typing_extensions import override from confirmation import settings as confirmation_settings from zerver.lib import cache from zerver.lib.cache import ( - active_non_guest_user_ids_cache_key, - active_user_ids_cache_key, - bot_dict_fields, - bot_dicts_in_realm_cache_key, - bot_profile_cache_key, cache_delete, cache_set, cache_with_key, @@ -73,15 +63,9 @@ from zerver.lib.cache import ( flush_stream, flush_submessage, flush_used_upload_space_cache, - flush_user_profile, get_realm_used_upload_space_cache_key, realm_alert_words_automaton_cache_key, realm_alert_words_cache_key, - realm_user_dict_fields, - realm_user_dicts_cache_key, - user_profile_by_api_key_cache_key, - user_profile_by_id_cache_key, - user_profile_cache_key, ) from zerver.lib.exceptions import JsonableError, RateLimitedError from zerver.lib.per_request_cache import ( @@ -97,10 +81,8 @@ from zerver.lib.types import ( FieldElement, GroupPermissionSetting, LinkifierDict, - ProfileData, ProfileDataElementBase, ProfileDataElementValue, - RawUserDict, RealmPlaygroundDict, RealmUserValidator, UnspecifiedValue, @@ -119,6 +101,10 @@ from zerver.lib.validator import ( validate_select_field, ) from zerver.models.constants import MAX_LANGUAGE_ID_LENGTH, MAX_TOPIC_NAME_LENGTH +from zerver.models.users import RealmUserDefault as RealmUserDefault +from zerver.models.users import UserBaseSettings as UserBaseSettings +from zerver.models.users import UserProfile as UserProfile +from zerver.models.users import get_user_profile_by_id_in_realm SECONDS_PER_DAY = 86400 @@ -1577,816 +1563,6 @@ class Recipient(models.Model): return self._type_names[self.type] -class UserBaseSettings(models.Model): - """This abstract class is the container for all preferences/personal - settings for users that control the behavior of the application. - - It was extracted from UserProfile to support the RealmUserDefault - model (i.e. allow individual realms to configure the default - values of these preferences for new users in their organization). - - Changing the default value for a field declared here likely - requires a migration to update all RealmUserDefault rows that had - the old default value to have the new default value. Otherwise, - the default change will only affect new users joining Realms - created after the change. - """ - - ### Generic UI settings - enter_sends = models.BooleanField(default=False) - - ### Preferences. ### - # left_side_userlist was removed from the UI in Zulip 6.0; the - # database model is being temporarily preserved in case we want to - # restore a version of the setting, preserving who had it enabled. - left_side_userlist = models.BooleanField(default=False) - default_language = models.CharField(default="en", max_length=MAX_LANGUAGE_ID_LENGTH) - # This setting controls which view is rendered first when Zulip loads. - # Values for it are URL suffix after `#`. - web_home_view = models.TextField(default="inbox") - web_escape_navigates_to_home_view = models.BooleanField(default=True) - dense_mode = models.BooleanField(default=True) - fluid_layout_width = models.BooleanField(default=False) - high_contrast_mode = models.BooleanField(default=False) - translate_emoticons = models.BooleanField(default=False) - display_emoji_reaction_users = models.BooleanField(default=True) - twenty_four_hour_time = models.BooleanField(default=False) - starred_message_counts = models.BooleanField(default=True) - COLOR_SCHEME_AUTOMATIC = 1 - COLOR_SCHEME_NIGHT = 2 - COLOR_SCHEME_LIGHT = 3 - COLOR_SCHEME_CHOICES = [COLOR_SCHEME_AUTOMATIC, COLOR_SCHEME_NIGHT, COLOR_SCHEME_LIGHT] - color_scheme = models.PositiveSmallIntegerField(default=COLOR_SCHEME_AUTOMATIC) - - # UI setting controlling Zulip's behavior of demoting in the sort - # order and graying out streams with no recent traffic. The - # default behavior, automatic, enables this behavior once a user - # is subscribed to 30+ streams in the web app. - DEMOTE_STREAMS_AUTOMATIC = 1 - DEMOTE_STREAMS_ALWAYS = 2 - DEMOTE_STREAMS_NEVER = 3 - DEMOTE_STREAMS_CHOICES = [ - DEMOTE_STREAMS_AUTOMATIC, - DEMOTE_STREAMS_ALWAYS, - DEMOTE_STREAMS_NEVER, - ] - demote_inactive_streams = models.PositiveSmallIntegerField(default=DEMOTE_STREAMS_AUTOMATIC) - - # UI setting controlling whether or not the Zulip web app will - # mark messages as read as it scrolls through the feed. - - MARK_READ_ON_SCROLL_ALWAYS = 1 - MARK_READ_ON_SCROLL_CONVERSATION_ONLY = 2 - MARK_READ_ON_SCROLL_NEVER = 3 - - WEB_MARK_READ_ON_SCROLL_POLICY_CHOICES = [ - MARK_READ_ON_SCROLL_ALWAYS, - MARK_READ_ON_SCROLL_CONVERSATION_ONLY, - MARK_READ_ON_SCROLL_NEVER, - ] - - web_mark_read_on_scroll_policy = models.SmallIntegerField(default=MARK_READ_ON_SCROLL_ALWAYS) - - # Emoji sets - GOOGLE_EMOJISET = "google" - GOOGLE_BLOB_EMOJISET = "google-blob" - TEXT_EMOJISET = "text" - TWITTER_EMOJISET = "twitter" - EMOJISET_CHOICES = ( - (GOOGLE_EMOJISET, "Google"), - (TWITTER_EMOJISET, "Twitter"), - (TEXT_EMOJISET, "Plain text"), - (GOOGLE_BLOB_EMOJISET, "Google blobs"), - ) - emojiset = models.CharField(default=GOOGLE_EMOJISET, choices=EMOJISET_CHOICES, max_length=20) - - # User list style - USER_LIST_STYLE_COMPACT = 1 - USER_LIST_STYLE_WITH_STATUS = 2 - USER_LIST_STYLE_WITH_AVATAR = 3 - USER_LIST_STYLE_CHOICES = [ - USER_LIST_STYLE_COMPACT, - USER_LIST_STYLE_WITH_STATUS, - USER_LIST_STYLE_WITH_AVATAR, - ] - user_list_style = models.PositiveSmallIntegerField(default=USER_LIST_STYLE_WITH_STATUS) - - # Show unread counts for - WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_ALL_STREAMS = 1 - WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_UNMUTED_STREAMS = 2 - WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_NO_STREAMS = 3 - WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_CHOICES = [ - WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_ALL_STREAMS, - WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_UNMUTED_STREAMS, - WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_NO_STREAMS, - ] - web_stream_unreads_count_display_policy = models.PositiveSmallIntegerField( - default=WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_UNMUTED_STREAMS - ) - - ### Notifications settings. ### - - email_notifications_batching_period_seconds = models.IntegerField(default=120) - - # Stream notifications. - enable_stream_desktop_notifications = models.BooleanField(default=False) - enable_stream_email_notifications = models.BooleanField(default=False) - enable_stream_push_notifications = models.BooleanField(default=False) - enable_stream_audible_notifications = models.BooleanField(default=False) - notification_sound = models.CharField(max_length=20, default="zulip") - wildcard_mentions_notify = models.BooleanField(default=True) - - # Followed Topics notifications. - enable_followed_topic_desktop_notifications = models.BooleanField(default=True) - enable_followed_topic_email_notifications = models.BooleanField(default=True) - enable_followed_topic_push_notifications = models.BooleanField(default=True) - enable_followed_topic_audible_notifications = models.BooleanField(default=True) - enable_followed_topic_wildcard_mentions_notify = models.BooleanField(default=True) - - # Direct message + @-mention notifications. - enable_desktop_notifications = models.BooleanField(default=True) - pm_content_in_desktop_notifications = models.BooleanField(default=True) - enable_sounds = models.BooleanField(default=True) - enable_offline_email_notifications = models.BooleanField(default=True) - message_content_in_email_notifications = models.BooleanField(default=True) - enable_offline_push_notifications = models.BooleanField(default=True) - enable_online_push_notifications = models.BooleanField(default=True) - - DESKTOP_ICON_COUNT_DISPLAY_MESSAGES = 1 - DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION_FOLLOWED_TOPIC = 2 - DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION = 3 - DESKTOP_ICON_COUNT_DISPLAY_NONE = 4 - DESKTOP_ICON_COUNT_DISPLAY_CHOICES = [ - DESKTOP_ICON_COUNT_DISPLAY_MESSAGES, - DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION, - DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION_FOLLOWED_TOPIC, - DESKTOP_ICON_COUNT_DISPLAY_NONE, - ] - desktop_icon_count_display = models.PositiveSmallIntegerField( - default=DESKTOP_ICON_COUNT_DISPLAY_MESSAGES - ) - - enable_digest_emails = models.BooleanField(default=True) - enable_login_emails = models.BooleanField(default=True) - enable_marketing_emails = models.BooleanField(default=True) - presence_enabled = models.BooleanField(default=True) - - REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_AUTOMATIC = 1 - REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_ALWAYS = 2 - REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_NEVER = 3 - REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES = [ - REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_AUTOMATIC, - REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_ALWAYS, - REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_NEVER, - ] - realm_name_in_email_notifications_policy = models.PositiveSmallIntegerField( - default=REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_AUTOMATIC - ) - - # The following two settings control which topics to automatically - # 'follow' or 'unmute in a muted stream', respectively. - # Follow or unmute a topic automatically on: - # - PARTICIPATION: Send a message, React to a message, Participate in a poll or Edit a TO-DO list. - # - SEND: Send a message. - # - INITIATION: Send the first message in the topic. - # - NEVER: Never automatically follow or unmute a topic. - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION = 1 - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND = 2 - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION = 3 - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER = 4 - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES = [ - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION, - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND, - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION, - AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER, - ] - automatically_follow_topics_policy = models.PositiveSmallIntegerField( - default=AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION, - ) - automatically_unmute_topics_in_muted_streams_policy = models.PositiveSmallIntegerField( - default=AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND, - ) - automatically_follow_topics_where_mentioned = models.BooleanField(default=True) - - # Whether or not the user wants to sync their drafts. - enable_drafts_synchronization = models.BooleanField(default=True) - - # Privacy settings - send_stream_typing_notifications = models.BooleanField(default=True) - send_private_typing_notifications = models.BooleanField(default=True) - send_read_receipts = models.BooleanField(default=True) - - # Who in the organization has access to users' actual email - # addresses. Controls whether the UserProfile.email field is - # the same as UserProfile.delivery_email, or is instead a fake - # generated value encoding the user ID and realm hostname. - EMAIL_ADDRESS_VISIBILITY_EVERYONE = 1 - EMAIL_ADDRESS_VISIBILITY_MEMBERS = 2 - EMAIL_ADDRESS_VISIBILITY_ADMINS = 3 - EMAIL_ADDRESS_VISIBILITY_NOBODY = 4 - EMAIL_ADDRESS_VISIBILITY_MODERATORS = 5 - email_address_visibility = models.PositiveSmallIntegerField( - default=EMAIL_ADDRESS_VISIBILITY_EVERYONE, - ) - - EMAIL_ADDRESS_VISIBILITY_ID_TO_NAME_MAP = { - EMAIL_ADDRESS_VISIBILITY_EVERYONE: gettext_lazy("Admins, moderators, members and guests"), - EMAIL_ADDRESS_VISIBILITY_MEMBERS: gettext_lazy("Admins, moderators and members"), - EMAIL_ADDRESS_VISIBILITY_MODERATORS: gettext_lazy("Admins and moderators"), - EMAIL_ADDRESS_VISIBILITY_ADMINS: gettext_lazy("Admins only"), - EMAIL_ADDRESS_VISIBILITY_NOBODY: gettext_lazy("Nobody"), - } - - EMAIL_ADDRESS_VISIBILITY_TYPES = list(EMAIL_ADDRESS_VISIBILITY_ID_TO_NAME_MAP.keys()) - - display_settings_legacy = dict( - # Don't add anything new to this legacy dict. - # Instead, see `modern_settings` below. - color_scheme=int, - default_language=str, - web_home_view=str, - demote_inactive_streams=int, - dense_mode=bool, - emojiset=str, - enable_drafts_synchronization=bool, - enter_sends=bool, - fluid_layout_width=bool, - high_contrast_mode=bool, - left_side_userlist=bool, - starred_message_counts=bool, - translate_emoticons=bool, - twenty_four_hour_time=bool, - ) - - notification_settings_legacy = dict( - # Don't add anything new to this legacy dict. - # Instead, see `modern_notification_settings` below. - desktop_icon_count_display=int, - email_notifications_batching_period_seconds=int, - enable_desktop_notifications=bool, - enable_digest_emails=bool, - enable_login_emails=bool, - enable_marketing_emails=bool, - enable_offline_email_notifications=bool, - enable_offline_push_notifications=bool, - enable_online_push_notifications=bool, - enable_sounds=bool, - enable_stream_audible_notifications=bool, - enable_stream_desktop_notifications=bool, - enable_stream_email_notifications=bool, - enable_stream_push_notifications=bool, - message_content_in_email_notifications=bool, - notification_sound=str, - pm_content_in_desktop_notifications=bool, - presence_enabled=bool, - realm_name_in_email_notifications_policy=int, - wildcard_mentions_notify=bool, - ) - - modern_settings = dict( - # Add new general settings here. - display_emoji_reaction_users=bool, - email_address_visibility=int, - web_escape_navigates_to_home_view=bool, - send_private_typing_notifications=bool, - send_read_receipts=bool, - send_stream_typing_notifications=bool, - web_mark_read_on_scroll_policy=int, - user_list_style=int, - web_stream_unreads_count_display_policy=int, - ) - - modern_notification_settings: Dict[str, Any] = dict( - # Add new notification settings here. - enable_followed_topic_desktop_notifications=bool, - enable_followed_topic_email_notifications=bool, - enable_followed_topic_push_notifications=bool, - enable_followed_topic_audible_notifications=bool, - enable_followed_topic_wildcard_mentions_notify=bool, - automatically_follow_topics_policy=int, - automatically_unmute_topics_in_muted_streams_policy=int, - automatically_follow_topics_where_mentioned=bool, - ) - - notification_setting_types = { - **notification_settings_legacy, - **modern_notification_settings, - } - - # Define the types of the various automatically managed properties - property_types = { - **display_settings_legacy, - **notification_setting_types, - **modern_settings, - } - - class Meta: - abstract = True - - @staticmethod - def emojiset_choices() -> List[Dict[str, str]]: - return [ - dict(key=emojiset[0], text=emojiset[1]) for emojiset in UserProfile.EMOJISET_CHOICES - ] - - -class RealmUserDefault(UserBaseSettings): - """This table stores realm-level default values for user preferences - like notification settings, used when creating a new user account. - """ - - realm = models.OneToOneField(Realm, on_delete=CASCADE) - - -class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): - USERNAME_FIELD = "email" - MAX_NAME_LENGTH = 100 - MIN_NAME_LENGTH = 2 - API_KEY_LENGTH = 32 - NAME_INVALID_CHARS = ["*", "`", "\\", ">", '"', "@"] - - DEFAULT_BOT = 1 - """ - Incoming webhook bots are limited to only sending messages via webhooks. - Thus, it is less of a security risk to expose their API keys to third-party services, - since they can't be used to read messages. - """ - INCOMING_WEBHOOK_BOT = 2 - # This value is also being used in web/src/settings_bots.js. - # On updating it here, update it there as well. - OUTGOING_WEBHOOK_BOT = 3 - """ - Embedded bots run within the Zulip server itself; events are added to the - embedded_bots queue and then handled by a QueueProcessingWorker. - """ - EMBEDDED_BOT = 4 - - BOT_TYPES = { - DEFAULT_BOT: "Generic bot", - INCOMING_WEBHOOK_BOT: "Incoming webhook", - OUTGOING_WEBHOOK_BOT: "Outgoing webhook", - EMBEDDED_BOT: "Embedded bot", - } - - SERVICE_BOT_TYPES = [ - OUTGOING_WEBHOOK_BOT, - EMBEDDED_BOT, - ] - - # For historical reasons, Zulip has two email fields. The - # `delivery_email` field is the user's email address, where all - # email notifications will be sent, and is used for all - # authentication use cases. - # - # The `email` field is the same as delivery_email in organizations - # with EMAIL_ADDRESS_VISIBILITY_EVERYONE. For other - # organizations, it will be a unique value of the form - # user1234@example.com. This field exists for backwards - # compatibility in Zulip APIs where users are referred to by their - # email address, not their ID; it should be used in all API use cases. - # - # Both fields are unique within a realm (in a case-insensitive - # fashion). Since Django's unique_together is case sensitive, this - # is enforced via SQL indexes created by - # zerver/migrations/0295_case_insensitive_email_indexes.py. - delivery_email = models.EmailField(blank=False, db_index=True) - email = models.EmailField(blank=False, db_index=True) - - realm = models.ForeignKey(Realm, on_delete=CASCADE) - # Foreign key to the Recipient object for PERSONAL type messages to this user. - recipient = models.ForeignKey(Recipient, null=True, on_delete=models.SET_NULL) - - INACCESSIBLE_USER_NAME = gettext_lazy("Unknown user") - # The user's name. We prefer the model of a full_name - # over first+last because cultures vary on how many - # names one has, whether the family name is first or last, etc. - # It also allows organizations to encode a bit of non-name data in - # the "name" attribute if desired, like gender pronouns, - # graduation year, etc. - full_name = models.CharField(max_length=MAX_NAME_LENGTH) - - date_joined = models.DateTimeField(default=timezone_now) - - # Terms of Service version number that this user has accepted. We - # use the special value TOS_VERSION_BEFORE_FIRST_LOGIN for users - # whose account was created without direct user interaction (via - # the API or a data import), and null for users whose account is - # fully created on servers that do not have a configured ToS. - TOS_VERSION_BEFORE_FIRST_LOGIN = "-1" - tos_version = models.CharField(null=True, max_length=10) - api_key = models.CharField(max_length=API_KEY_LENGTH, default=generate_api_key, unique=True) - - # A UUID generated on user creation. Introduced primarily to - # provide a unique key for a user for the mobile push - # notifications bouncer that will not have collisions after doing - # a data export and then import. - uuid = models.UUIDField(default=uuid4, unique=True) - - # Whether the user has access to server-level administrator pages, like /activity - is_staff = models.BooleanField(default=False) - - # For a normal user, this is True unless the user or an admin has - # deactivated their account. The name comes from Django; this field - # isn't related to presence or to whether the user has recently used Zulip. - # - # See also `long_term_idle`. - is_active = models.BooleanField(default=True, db_index=True) - - is_billing_admin = models.BooleanField(default=False, db_index=True) - - is_bot = models.BooleanField(default=False, db_index=True) - bot_type = models.PositiveSmallIntegerField(null=True, db_index=True) - bot_owner = models.ForeignKey("self", null=True, on_delete=models.SET_NULL) - - # Each role has a superset of the permissions of the next higher - # numbered role. When adding new roles, leave enough space for - # future roles to be inserted between currently adjacent - # roles. These constants appear in RealmAuditLog.extra_data, so - # changes to them will require a migration of RealmAuditLog. - ROLE_REALM_OWNER = 100 - ROLE_REALM_ADMINISTRATOR = 200 - ROLE_MODERATOR = 300 - ROLE_MEMBER = 400 - ROLE_GUEST = 600 - role = models.PositiveSmallIntegerField(default=ROLE_MEMBER, db_index=True) - - ROLE_TYPES = [ - ROLE_REALM_OWNER, - ROLE_REALM_ADMINISTRATOR, - ROLE_MODERATOR, - ROLE_MEMBER, - ROLE_GUEST, - ] - - # Whether the user has been "soft-deactivated" due to weeks of inactivity. - # For these users we avoid doing UserMessage table work, as an optimization - # for large Zulip organizations with lots of single-visit users. - long_term_idle = models.BooleanField(default=False, db_index=True) - - # When we last added basic UserMessage rows for a long_term_idle user. - last_active_message_id = models.IntegerField(null=True) - - # Mirror dummies are fake (!is_active) users used to provide - # message senders in our cross-protocol Zephyr<->Zulip content - # mirroring integration, so that we can display mirrored content - # like native Zulip messages (with a name + avatar, etc.). - is_mirror_dummy = models.BooleanField(default=False) - - # Users with this flag set are allowed to forge messages as sent by another - # user and to send to private streams; also used for Zephyr/Jabber mirroring. - can_forge_sender = models.BooleanField(default=False, db_index=True) - # Users with this flag set can create other users via API. - can_create_users = models.BooleanField(default=False, db_index=True) - - # Used for rate-limiting certain automated messages generated by bots - last_reminder = models.DateTimeField(default=None, null=True) - - # Minutes to wait before warning a bot owner that their bot sent a message - # to a nonexistent stream - BOT_OWNER_STREAM_ALERT_WAITPERIOD = 1 - - # API rate limits, formatted as a comma-separated list of range:max pairs - rate_limits = models.CharField(default="", max_length=100) - - # Default streams for some deprecated/legacy classes of bot users. - default_sending_stream = models.ForeignKey( - "zerver.Stream", - null=True, - related_name="+", - on_delete=models.SET_NULL, - ) - default_events_register_stream = models.ForeignKey( - "zerver.Stream", - null=True, - related_name="+", - on_delete=models.SET_NULL, - ) - default_all_public_streams = models.BooleanField(default=False) - - # A time zone name from the `tzdata` database, as found in zoneinfo.available_timezones(). - # - # The longest existing name is 32 characters long, so max_length=40 seems - # like a safe choice. - # - # In Django, the convention is to use an empty string instead of NULL/None - # for text-based fields. For more information, see - # https://docs.djangoproject.com/en/3.2/ref/models/fields/#django.db.models.Field.null. - timezone = models.CharField(max_length=40, default="") - - AVATAR_FROM_GRAVATAR = "G" - AVATAR_FROM_USER = "U" - AVATAR_SOURCES = ( - (AVATAR_FROM_GRAVATAR, "Hosted by Gravatar"), - (AVATAR_FROM_USER, "Uploaded by user"), - ) - avatar_source = models.CharField( - default=AVATAR_FROM_GRAVATAR, choices=AVATAR_SOURCES, max_length=1 - ) - avatar_version = models.PositiveSmallIntegerField(default=1) - avatar_hash = models.CharField(null=True, max_length=64) - - # TODO: TUTORIAL_STATUS was originally an optimization designed to - # allow us to skip querying the OnboardingStep table when loading - # /. This optimization is no longer effective, so it's possible we - # should delete it. - TUTORIAL_WAITING = "W" - TUTORIAL_STARTED = "S" - TUTORIAL_FINISHED = "F" - TUTORIAL_STATES = ( - (TUTORIAL_WAITING, "Waiting"), - (TUTORIAL_STARTED, "Started"), - (TUTORIAL_FINISHED, "Finished"), - ) - tutorial_status = models.CharField( - default=TUTORIAL_WAITING, choices=TUTORIAL_STATES, max_length=1 - ) - - # Contains serialized JSON of the form: - # [("step 1", true), ("step 2", false)] - # where the second element of each tuple is if the step has been - # completed. - onboarding_steps = models.TextField(default="[]") - - zoom_token = models.JSONField(default=None, null=True) - - objects = UserManager() - - ROLE_ID_TO_NAME_MAP = { - ROLE_REALM_OWNER: gettext_lazy("Organization owner"), - ROLE_REALM_ADMINISTRATOR: gettext_lazy("Organization administrator"), - ROLE_MODERATOR: gettext_lazy("Moderator"), - ROLE_MEMBER: gettext_lazy("Member"), - ROLE_GUEST: gettext_lazy("Guest"), - } - - def get_role_name(self) -> str: - return str(self.ROLE_ID_TO_NAME_MAP[self.role]) - - def profile_data(self) -> ProfileData: - values = CustomProfileFieldValue.objects.filter(user_profile=self) - user_data = { - v.field_id: {"value": v.value, "rendered_value": v.rendered_value} for v in values - } - data: ProfileData = [] - for field in custom_profile_fields_for_realm(self.realm_id): - field_values = user_data.get(field.id, None) - if field_values: - value, rendered_value = field_values.get("value"), field_values.get( - "rendered_value" - ) - else: - value, rendered_value = None, None - field_type = field.field_type - if value is not None: - converter = field.FIELD_CONVERTERS[field_type] - value = converter(value) - - field_data = field.as_dict() - data.append( - { - "id": field_data["id"], - "name": field_data["name"], - "type": field_data["type"], - "hint": field_data["hint"], - "field_data": field_data["field_data"], - "order": field_data["order"], - "value": value, - "rendered_value": rendered_value, - } - ) - - return data - - def can_admin_user(self, target_user: "UserProfile") -> bool: - """Returns whether this user has permission to modify target_user""" - if target_user.bot_owner_id == self.id: - return True - elif self.is_realm_admin and self.realm == target_user.realm: - return True - else: - return False - - @override - def __str__(self) -> str: - return f"{self.email} {self.realm!r}" - - @property - def is_provisional_member(self) -> bool: - if self.is_moderator: - return False - diff = (timezone_now() - self.date_joined).days - if diff < self.realm.waiting_period_threshold: - return True - return False - - @property - def is_realm_admin(self) -> bool: - return self.role in (UserProfile.ROLE_REALM_ADMINISTRATOR, UserProfile.ROLE_REALM_OWNER) - - @is_realm_admin.setter - def is_realm_admin(self, value: bool) -> None: - if value: - self.role = UserProfile.ROLE_REALM_ADMINISTRATOR - elif self.role == UserProfile.ROLE_REALM_ADMINISTRATOR: - # We need to be careful to not accidentally change - # ROLE_GUEST to ROLE_MEMBER here. - self.role = UserProfile.ROLE_MEMBER - - @property - def has_billing_access(self) -> bool: - return self.is_realm_owner or self.is_billing_admin - - @property - def is_realm_owner(self) -> bool: - return self.role == UserProfile.ROLE_REALM_OWNER - - @is_realm_owner.setter - def is_realm_owner(self, value: bool) -> None: - if value: - self.role = UserProfile.ROLE_REALM_OWNER - elif self.role == UserProfile.ROLE_REALM_OWNER: - # We need to be careful to not accidentally change - # ROLE_GUEST to ROLE_MEMBER here. - self.role = UserProfile.ROLE_MEMBER - - @property - def is_guest(self) -> bool: - return self.role == UserProfile.ROLE_GUEST - - @is_guest.setter - def is_guest(self, value: bool) -> None: - if value: - self.role = UserProfile.ROLE_GUEST - elif self.role == UserProfile.ROLE_GUEST: - # We need to be careful to not accidentally change - # ROLE_REALM_ADMINISTRATOR to ROLE_MEMBER here. - self.role = UserProfile.ROLE_MEMBER - - @property - def is_moderator(self) -> bool: - return self.role == UserProfile.ROLE_MODERATOR - - @is_moderator.setter - def is_moderator(self, value: bool) -> None: - if value: - self.role = UserProfile.ROLE_MODERATOR - elif self.role == UserProfile.ROLE_MODERATOR: - # We need to be careful to not accidentally change - # ROLE_GUEST to ROLE_MEMBER here. - self.role = UserProfile.ROLE_MEMBER - - @property - def is_incoming_webhook(self) -> bool: - return self.bot_type == UserProfile.INCOMING_WEBHOOK_BOT - - @property - def allowed_bot_types(self) -> List[int]: - allowed_bot_types = [] - if ( - self.is_realm_admin - or self.realm.bot_creation_policy != Realm.BOT_CREATION_LIMIT_GENERIC_BOTS - ): - allowed_bot_types.append(UserProfile.DEFAULT_BOT) - allowed_bot_types += [ - UserProfile.INCOMING_WEBHOOK_BOT, - UserProfile.OUTGOING_WEBHOOK_BOT, - ] - if settings.EMBEDDED_BOTS_ENABLED: - allowed_bot_types.append(UserProfile.EMBEDDED_BOT) - return allowed_bot_types - - def email_address_is_realm_public(self) -> bool: - # Bots always have EMAIL_ADDRESS_VISIBILITY_EVERYONE. - if self.email_address_visibility == UserProfile.EMAIL_ADDRESS_VISIBILITY_EVERYONE: - return True - return False - - def has_permission(self, policy_name: str) -> bool: - from zerver.lib.user_groups import is_user_in_group - - if policy_name not in [ - "add_custom_emoji_policy", - "create_multiuse_invite_group", - "create_private_stream_policy", - "create_public_stream_policy", - "create_web_public_stream_policy", - "delete_own_message_policy", - "edit_topic_policy", - "invite_to_stream_policy", - "invite_to_realm_policy", - "move_messages_between_streams_policy", - "user_group_edit_policy", - ]: - raise AssertionError("Invalid policy") - - if policy_name in Realm.REALM_PERMISSION_GROUP_SETTINGS: - allowed_user_group = getattr(self.realm, policy_name) - return is_user_in_group(allowed_user_group, self) - - policy_value = getattr(self.realm, policy_name) - if policy_value == Realm.POLICY_NOBODY: - return False - - if policy_value == Realm.POLICY_EVERYONE: - return True - - if self.is_realm_owner: - return True - - if policy_value == Realm.POLICY_OWNERS_ONLY: - return False - - if self.is_realm_admin: - return True - - if policy_value == Realm.POLICY_ADMINS_ONLY: - return False - - if self.is_moderator: - return True - - if policy_value == Realm.POLICY_MODERATORS_ONLY: - return False - - if self.is_guest: - return False - - if policy_value == Realm.POLICY_MEMBERS_ONLY: - return True - - assert policy_value == Realm.POLICY_FULL_MEMBERS_ONLY - return not self.is_provisional_member - - def can_create_public_streams(self) -> bool: - return self.has_permission("create_public_stream_policy") - - def can_create_private_streams(self) -> bool: - return self.has_permission("create_private_stream_policy") - - def can_create_web_public_streams(self) -> bool: - if not self.realm.web_public_streams_enabled(): - return False - return self.has_permission("create_web_public_stream_policy") - - def can_subscribe_other_users(self) -> bool: - return self.has_permission("invite_to_stream_policy") - - def can_invite_users_by_email(self) -> bool: - return self.has_permission("invite_to_realm_policy") - - def can_create_multiuse_invite_to_realm(self) -> bool: - return self.has_permission("create_multiuse_invite_group") - - def can_move_messages_between_streams(self) -> bool: - return self.has_permission("move_messages_between_streams_policy") - - def can_edit_user_groups(self) -> bool: - return self.has_permission("user_group_edit_policy") - - def can_move_messages_to_another_topic(self) -> bool: - return self.has_permission("edit_topic_policy") - - def can_add_custom_emoji(self) -> bool: - return self.has_permission("add_custom_emoji_policy") - - def can_delete_own_message(self) -> bool: - return self.has_permission("delete_own_message_policy") - - def can_access_public_streams(self) -> bool: - return not (self.is_guest or self.realm.is_zephyr_mirror_realm) - - def major_tos_version(self) -> int: - if self.tos_version is not None: - return int(self.tos_version.split(".")[0]) - else: - return -1 - - def format_requester_for_logs(self) -> str: - return "{}@{}".format(self.id, self.realm.string_id or "root") - - @override - def set_password(self, password: Optional[str]) -> None: - if password is None: - self.set_unusable_password() - return - - from zproject.backends import check_password_strength - - if not check_password_strength(password): - raise PasswordTooWeakError - - super().set_password(password) - - class Meta: - indexes = [ - models.Index(Upper("email"), name="upper_userprofile_email_idx"), - ] - - -class PasswordTooWeakError(Exception): - pass - - class UserGroup(models.Model): # type: ignore[django-manager-missing] # django-stubs cannot resolve the custom CTEManager yet https://github.com/typeddjango/django-stubs/issues/1023 MAX_NAME_LENGTH = 100 INVALID_NAME_PREFIXES = ["@", "role:", "user:", "stream:", "channel:"] @@ -2472,17 +1648,6 @@ class GroupGroupMembership(models.Model): ] -def remote_user_to_email(remote_user: str) -> str: - if settings.SSO_APPEND_DOMAIN is not None: - return Address(username=remote_user, domain=settings.SSO_APPEND_DOMAIN).addr_spec - return remote_user - - -# Make sure we flush the UserProfile object from our remote cache -# whenever we save it. -post_save.connect(flush_user_profile, sender=UserProfile) - - class PreregistrationRealm(models.Model): """Data on a partially created realm entered by a user who has completed the "new organization" form. Used to transfer the user's @@ -4089,218 +3254,6 @@ class Subscription(models.Model): ] -@cache_with_key(user_profile_by_id_cache_key, timeout=3600 * 24 * 7) -def get_user_profile_by_id(user_profile_id: int) -> UserProfile: - return UserProfile.objects.select_related( - "realm", "realm__can_access_all_users_group", "bot_owner" - ).get(id=user_profile_id) - - -def get_user_profile_by_email(email: str) -> UserProfile: - """This function is intended to be used for - manual manage.py shell work; robust code must use get_user or - get_user_by_delivery_email instead, because Zulip supports - multiple users with a given (delivery) email address existing on a - single server (in different realms). - """ - return UserProfile.objects.select_related("realm").get(delivery_email__iexact=email.strip()) - - -@cache_with_key(user_profile_by_api_key_cache_key, timeout=3600 * 24 * 7) -def maybe_get_user_profile_by_api_key(api_key: str) -> Optional[UserProfile]: - try: - return UserProfile.objects.select_related( - "realm", "realm__can_access_all_users_group", "bot_owner" - ).get(api_key=api_key) - except UserProfile.DoesNotExist: - # We will cache failed lookups with None. The - # use case here is that broken API clients may - # continually ask for the same wrong API key, and - # we want to handle that as quickly as possible. - return None - - -def get_user_profile_by_api_key(api_key: str) -> UserProfile: - user_profile = maybe_get_user_profile_by_api_key(api_key) - if user_profile is None: - raise UserProfile.DoesNotExist - - return user_profile - - -def get_user_by_delivery_email(email: str, realm: Realm) -> UserProfile: - """Fetches a user given their delivery email. For use in - authentication/registration contexts. Do not use for user-facing - views (e.g. Zulip API endpoints) as doing so would violate the - EMAIL_ADDRESS_VISIBILITY_ADMINS security model. Use get_user in - those code paths. - """ - return UserProfile.objects.select_related( - "realm", "realm__can_access_all_users_group", "bot_owner" - ).get(delivery_email__iexact=email.strip(), realm=realm) - - -def get_users_by_delivery_email(emails: Set[str], realm: Realm) -> QuerySet[UserProfile]: - """This is similar to get_user_by_delivery_email, and - it has the same security caveats. It gets multiple - users and returns a QuerySet, since most callers - will only need two or three fields. - - If you are using this to get large UserProfile objects, you are - probably making a mistake, but if you must, - then use `select_related`. - """ - - """ - Django doesn't support delivery_email__iexact__in, so - we simply OR all the filters that we'd do for the - one-email case. - """ - email_filter = Q() - for email in emails: - email_filter |= Q(delivery_email__iexact=email.strip()) - - return UserProfile.objects.filter(realm=realm).filter(email_filter) - - -@cache_with_key(user_profile_cache_key, timeout=3600 * 24 * 7) -def get_user(email: str, realm: Realm) -> UserProfile: - """Fetches the user by its visible-to-other users username (in the - `email` field). For use in API contexts; do not use in - authentication/registration contexts as doing so will break - authentication in organizations using - EMAIL_ADDRESS_VISIBILITY_ADMINS. In those code paths, use - get_user_by_delivery_email. - """ - return UserProfile.objects.select_related( - "realm", "realm__can_access_all_users_group", "bot_owner" - ).get(email__iexact=email.strip(), realm=realm) - - -def get_active_user(email: str, realm: Realm) -> UserProfile: - """Variant of get_user_by_email that excludes deactivated users. - See get_user docstring for important usage notes.""" - user_profile = get_user(email, realm) - if not user_profile.is_active: - raise UserProfile.DoesNotExist - return user_profile - - -def get_user_profile_by_id_in_realm(uid: int, realm: Realm) -> UserProfile: - return UserProfile.objects.select_related( - "realm", "realm__can_access_all_users_group", "bot_owner" - ).get(id=uid, realm=realm) - - -def get_active_user_profile_by_id_in_realm(uid: int, realm: Realm) -> UserProfile: - user_profile = get_user_profile_by_id_in_realm(uid, realm) - if not user_profile.is_active: - raise UserProfile.DoesNotExist - return user_profile - - -def get_user_including_cross_realm(email: str, realm: Realm) -> UserProfile: - if is_cross_realm_bot_email(email): - return get_system_bot(email, realm.id) - assert realm is not None - return get_user(email, realm) - - -@cache_with_key(bot_profile_cache_key, timeout=3600 * 24 * 7) -def get_system_bot(email: str, realm_id: int) -> UserProfile: - """ - This function doesn't use the realm_id argument yet, but requires - passing it as preparation for adding system bots to each realm instead - of having them all in a separate system bot realm. - If you're calling this function, use the id of the realm in which the system - bot will be after that migration. If the bot is supposed to send a message, - the same realm as the one *to* which the message will be sent should be used - because - cross-realm messages will be eliminated as part of the migration. - """ - return UserProfile.objects.select_related("realm").get(email__iexact=email.strip()) - - -def get_user_by_id_in_realm_including_cross_realm( - uid: int, - realm: Optional[Realm], -) -> UserProfile: - user_profile = get_user_profile_by_id(uid) - if user_profile.realm == realm: - return user_profile - - # Note: This doesn't validate whether the `realm` passed in is - # None/invalid for the is_cross_realm_bot_email case. - if is_cross_realm_bot_email(user_profile.delivery_email): - return user_profile - - raise UserProfile.DoesNotExist - - -@cache_with_key(realm_user_dicts_cache_key, timeout=3600 * 24 * 7) -def get_realm_user_dicts(realm_id: int) -> List[RawUserDict]: - return list( - UserProfile.objects.filter( - realm_id=realm_id, - ).values(*realm_user_dict_fields) - ) - - -@cache_with_key(active_user_ids_cache_key, timeout=3600 * 24 * 7) -def active_user_ids(realm_id: int) -> List[int]: - query = UserProfile.objects.filter( - realm_id=realm_id, - is_active=True, - ).values_list("id", flat=True) - return list(query) - - -@cache_with_key(active_non_guest_user_ids_cache_key, timeout=3600 * 24 * 7) -def active_non_guest_user_ids(realm_id: int) -> List[int]: - query = ( - UserProfile.objects.filter( - realm_id=realm_id, - is_active=True, - ) - .exclude( - role=UserProfile.ROLE_GUEST, - ) - .values_list("id", flat=True) - ) - return list(query) - - -def bot_owner_user_ids(user_profile: UserProfile) -> Set[int]: - is_private_bot = ( - user_profile.default_sending_stream - and user_profile.default_sending_stream.invite_only - or user_profile.default_events_register_stream - and user_profile.default_events_register_stream.invite_only - ) - assert user_profile.bot_owner_id is not None - if is_private_bot: - return {user_profile.bot_owner_id} - else: - users = {user.id for user in user_profile.realm.get_human_admin_users()} - users.add(user_profile.bot_owner_id) - return users - - -def get_source_profile(email: str, realm_id: int) -> Optional[UserProfile]: - try: - return get_user_by_delivery_email(email, get_realm_by_id(realm_id)) - except (Realm.DoesNotExist, UserProfile.DoesNotExist): - return None - - -@cache_with_key(lambda realm: bot_dicts_in_realm_cache_key(realm.id), timeout=3600 * 24 * 7) -def get_bot_dicts_in_realm(realm: Realm) -> List[Dict[str, Any]]: - return list(UserProfile.objects.filter(realm=realm, is_bot=True).values(*bot_dict_fields)) - - -def is_cross_realm_bot_email(email: str) -> bool: - return email.lower() in settings.CROSS_REALM_BOT_EMAILS - - class Huddle(models.Model): """ Represents a group of individuals who may have a diff --git a/zerver/models/users.py b/zerver/models/users.py new file mode 100644 index 0000000000..bfb74787d3 --- /dev/null +++ b/zerver/models/users.py @@ -0,0 +1,1077 @@ +# https://github.com/typeddjango/django-stubs/issues/1698 +# mypy: disable-error-code="explicit-override" + +from email.headerregistry import Address +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set +from uuid import uuid4 + +from django.conf import settings +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, UserManager +from django.db import models +from django.db.models import CASCADE, Q, QuerySet +from django.db.models.functions import Upper +from django.db.models.signals import post_save +from django.utils.timezone import now as timezone_now +from django.utils.translation import gettext_lazy +from typing_extensions import override + +from zerver.lib.cache import ( + active_non_guest_user_ids_cache_key, + active_user_ids_cache_key, + bot_dict_fields, + bot_dicts_in_realm_cache_key, + bot_profile_cache_key, + cache_with_key, + flush_user_profile, + realm_user_dict_fields, + realm_user_dicts_cache_key, + user_profile_by_api_key_cache_key, + user_profile_by_id_cache_key, + user_profile_cache_key, +) +from zerver.lib.types import ProfileData, RawUserDict +from zerver.lib.utils import generate_api_key +from zerver.models.constants import MAX_LANGUAGE_ID_LENGTH + +if TYPE_CHECKING: + from zerver.models import Realm + + +class UserBaseSettings(models.Model): + """This abstract class is the container for all preferences/personal + settings for users that control the behavior of the application. + + It was extracted from UserProfile to support the RealmUserDefault + model (i.e. allow individual realms to configure the default + values of these preferences for new users in their organization). + + Changing the default value for a field declared here likely + requires a migration to update all RealmUserDefault rows that had + the old default value to have the new default value. Otherwise, + the default change will only affect new users joining Realms + created after the change. + """ + + ### Generic UI settings + enter_sends = models.BooleanField(default=False) + + ### Preferences. ### + # left_side_userlist was removed from the UI in Zulip 6.0; the + # database model is being temporarily preserved in case we want to + # restore a version of the setting, preserving who had it enabled. + left_side_userlist = models.BooleanField(default=False) + default_language = models.CharField(default="en", max_length=MAX_LANGUAGE_ID_LENGTH) + # This setting controls which view is rendered first when Zulip loads. + # Values for it are URL suffix after `#`. + web_home_view = models.TextField(default="inbox") + web_escape_navigates_to_home_view = models.BooleanField(default=True) + dense_mode = models.BooleanField(default=True) + fluid_layout_width = models.BooleanField(default=False) + high_contrast_mode = models.BooleanField(default=False) + translate_emoticons = models.BooleanField(default=False) + display_emoji_reaction_users = models.BooleanField(default=True) + twenty_four_hour_time = models.BooleanField(default=False) + starred_message_counts = models.BooleanField(default=True) + COLOR_SCHEME_AUTOMATIC = 1 + COLOR_SCHEME_NIGHT = 2 + COLOR_SCHEME_LIGHT = 3 + COLOR_SCHEME_CHOICES = [COLOR_SCHEME_AUTOMATIC, COLOR_SCHEME_NIGHT, COLOR_SCHEME_LIGHT] + color_scheme = models.PositiveSmallIntegerField(default=COLOR_SCHEME_AUTOMATIC) + + # UI setting controlling Zulip's behavior of demoting in the sort + # order and graying out streams with no recent traffic. The + # default behavior, automatic, enables this behavior once a user + # is subscribed to 30+ streams in the web app. + DEMOTE_STREAMS_AUTOMATIC = 1 + DEMOTE_STREAMS_ALWAYS = 2 + DEMOTE_STREAMS_NEVER = 3 + DEMOTE_STREAMS_CHOICES = [ + DEMOTE_STREAMS_AUTOMATIC, + DEMOTE_STREAMS_ALWAYS, + DEMOTE_STREAMS_NEVER, + ] + demote_inactive_streams = models.PositiveSmallIntegerField(default=DEMOTE_STREAMS_AUTOMATIC) + + # UI setting controlling whether or not the Zulip web app will + # mark messages as read as it scrolls through the feed. + + MARK_READ_ON_SCROLL_ALWAYS = 1 + MARK_READ_ON_SCROLL_CONVERSATION_ONLY = 2 + MARK_READ_ON_SCROLL_NEVER = 3 + + WEB_MARK_READ_ON_SCROLL_POLICY_CHOICES = [ + MARK_READ_ON_SCROLL_ALWAYS, + MARK_READ_ON_SCROLL_CONVERSATION_ONLY, + MARK_READ_ON_SCROLL_NEVER, + ] + + web_mark_read_on_scroll_policy = models.SmallIntegerField(default=MARK_READ_ON_SCROLL_ALWAYS) + + # Emoji sets + GOOGLE_EMOJISET = "google" + GOOGLE_BLOB_EMOJISET = "google-blob" + TEXT_EMOJISET = "text" + TWITTER_EMOJISET = "twitter" + EMOJISET_CHOICES = ( + (GOOGLE_EMOJISET, "Google"), + (TWITTER_EMOJISET, "Twitter"), + (TEXT_EMOJISET, "Plain text"), + (GOOGLE_BLOB_EMOJISET, "Google blobs"), + ) + emojiset = models.CharField(default=GOOGLE_EMOJISET, choices=EMOJISET_CHOICES, max_length=20) + + # User list style + USER_LIST_STYLE_COMPACT = 1 + USER_LIST_STYLE_WITH_STATUS = 2 + USER_LIST_STYLE_WITH_AVATAR = 3 + USER_LIST_STYLE_CHOICES = [ + USER_LIST_STYLE_COMPACT, + USER_LIST_STYLE_WITH_STATUS, + USER_LIST_STYLE_WITH_AVATAR, + ] + user_list_style = models.PositiveSmallIntegerField(default=USER_LIST_STYLE_WITH_STATUS) + + # Show unread counts for + WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_ALL_STREAMS = 1 + WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_UNMUTED_STREAMS = 2 + WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_NO_STREAMS = 3 + WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_CHOICES = [ + WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_ALL_STREAMS, + WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_UNMUTED_STREAMS, + WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_NO_STREAMS, + ] + web_stream_unreads_count_display_policy = models.PositiveSmallIntegerField( + default=WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_UNMUTED_STREAMS + ) + + ### Notifications settings. ### + + email_notifications_batching_period_seconds = models.IntegerField(default=120) + + # Stream notifications. + enable_stream_desktop_notifications = models.BooleanField(default=False) + enable_stream_email_notifications = models.BooleanField(default=False) + enable_stream_push_notifications = models.BooleanField(default=False) + enable_stream_audible_notifications = models.BooleanField(default=False) + notification_sound = models.CharField(max_length=20, default="zulip") + wildcard_mentions_notify = models.BooleanField(default=True) + + # Followed Topics notifications. + enable_followed_topic_desktop_notifications = models.BooleanField(default=True) + enable_followed_topic_email_notifications = models.BooleanField(default=True) + enable_followed_topic_push_notifications = models.BooleanField(default=True) + enable_followed_topic_audible_notifications = models.BooleanField(default=True) + enable_followed_topic_wildcard_mentions_notify = models.BooleanField(default=True) + + # Direct message + @-mention notifications. + enable_desktop_notifications = models.BooleanField(default=True) + pm_content_in_desktop_notifications = models.BooleanField(default=True) + enable_sounds = models.BooleanField(default=True) + enable_offline_email_notifications = models.BooleanField(default=True) + message_content_in_email_notifications = models.BooleanField(default=True) + enable_offline_push_notifications = models.BooleanField(default=True) + enable_online_push_notifications = models.BooleanField(default=True) + + DESKTOP_ICON_COUNT_DISPLAY_MESSAGES = 1 + DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION_FOLLOWED_TOPIC = 2 + DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION = 3 + DESKTOP_ICON_COUNT_DISPLAY_NONE = 4 + DESKTOP_ICON_COUNT_DISPLAY_CHOICES = [ + DESKTOP_ICON_COUNT_DISPLAY_MESSAGES, + DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION, + DESKTOP_ICON_COUNT_DISPLAY_DM_MENTION_FOLLOWED_TOPIC, + DESKTOP_ICON_COUNT_DISPLAY_NONE, + ] + desktop_icon_count_display = models.PositiveSmallIntegerField( + default=DESKTOP_ICON_COUNT_DISPLAY_MESSAGES + ) + + enable_digest_emails = models.BooleanField(default=True) + enable_login_emails = models.BooleanField(default=True) + enable_marketing_emails = models.BooleanField(default=True) + presence_enabled = models.BooleanField(default=True) + + REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_AUTOMATIC = 1 + REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_ALWAYS = 2 + REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_NEVER = 3 + REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES = [ + REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_AUTOMATIC, + REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_ALWAYS, + REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_NEVER, + ] + realm_name_in_email_notifications_policy = models.PositiveSmallIntegerField( + default=REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_AUTOMATIC + ) + + # The following two settings control which topics to automatically + # 'follow' or 'unmute in a muted stream', respectively. + # Follow or unmute a topic automatically on: + # - PARTICIPATION: Send a message, React to a message, Participate in a poll or Edit a TO-DO list. + # - SEND: Send a message. + # - INITIATION: Send the first message in the topic. + # - NEVER: Never automatically follow or unmute a topic. + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION = 1 + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND = 2 + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION = 3 + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER = 4 + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES = [ + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION, + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND, + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION, + AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER, + ] + automatically_follow_topics_policy = models.PositiveSmallIntegerField( + default=AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION, + ) + automatically_unmute_topics_in_muted_streams_policy = models.PositiveSmallIntegerField( + default=AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND, + ) + automatically_follow_topics_where_mentioned = models.BooleanField(default=True) + + # Whether or not the user wants to sync their drafts. + enable_drafts_synchronization = models.BooleanField(default=True) + + # Privacy settings + send_stream_typing_notifications = models.BooleanField(default=True) + send_private_typing_notifications = models.BooleanField(default=True) + send_read_receipts = models.BooleanField(default=True) + + # Who in the organization has access to users' actual email + # addresses. Controls whether the UserProfile.email field is + # the same as UserProfile.delivery_email, or is instead a fake + # generated value encoding the user ID and realm hostname. + EMAIL_ADDRESS_VISIBILITY_EVERYONE = 1 + EMAIL_ADDRESS_VISIBILITY_MEMBERS = 2 + EMAIL_ADDRESS_VISIBILITY_ADMINS = 3 + EMAIL_ADDRESS_VISIBILITY_NOBODY = 4 + EMAIL_ADDRESS_VISIBILITY_MODERATORS = 5 + email_address_visibility = models.PositiveSmallIntegerField( + default=EMAIL_ADDRESS_VISIBILITY_EVERYONE, + ) + + EMAIL_ADDRESS_VISIBILITY_ID_TO_NAME_MAP = { + EMAIL_ADDRESS_VISIBILITY_EVERYONE: gettext_lazy("Admins, moderators, members and guests"), + EMAIL_ADDRESS_VISIBILITY_MEMBERS: gettext_lazy("Admins, moderators and members"), + EMAIL_ADDRESS_VISIBILITY_MODERATORS: gettext_lazy("Admins and moderators"), + EMAIL_ADDRESS_VISIBILITY_ADMINS: gettext_lazy("Admins only"), + EMAIL_ADDRESS_VISIBILITY_NOBODY: gettext_lazy("Nobody"), + } + + EMAIL_ADDRESS_VISIBILITY_TYPES = list(EMAIL_ADDRESS_VISIBILITY_ID_TO_NAME_MAP.keys()) + + display_settings_legacy = dict( + # Don't add anything new to this legacy dict. + # Instead, see `modern_settings` below. + color_scheme=int, + default_language=str, + web_home_view=str, + demote_inactive_streams=int, + dense_mode=bool, + emojiset=str, + enable_drafts_synchronization=bool, + enter_sends=bool, + fluid_layout_width=bool, + high_contrast_mode=bool, + left_side_userlist=bool, + starred_message_counts=bool, + translate_emoticons=bool, + twenty_four_hour_time=bool, + ) + + notification_settings_legacy = dict( + # Don't add anything new to this legacy dict. + # Instead, see `modern_notification_settings` below. + desktop_icon_count_display=int, + email_notifications_batching_period_seconds=int, + enable_desktop_notifications=bool, + enable_digest_emails=bool, + enable_login_emails=bool, + enable_marketing_emails=bool, + enable_offline_email_notifications=bool, + enable_offline_push_notifications=bool, + enable_online_push_notifications=bool, + enable_sounds=bool, + enable_stream_audible_notifications=bool, + enable_stream_desktop_notifications=bool, + enable_stream_email_notifications=bool, + enable_stream_push_notifications=bool, + message_content_in_email_notifications=bool, + notification_sound=str, + pm_content_in_desktop_notifications=bool, + presence_enabled=bool, + realm_name_in_email_notifications_policy=int, + wildcard_mentions_notify=bool, + ) + + modern_settings = dict( + # Add new general settings here. + display_emoji_reaction_users=bool, + email_address_visibility=int, + web_escape_navigates_to_home_view=bool, + send_private_typing_notifications=bool, + send_read_receipts=bool, + send_stream_typing_notifications=bool, + web_mark_read_on_scroll_policy=int, + user_list_style=int, + web_stream_unreads_count_display_policy=int, + ) + + modern_notification_settings: Dict[str, Any] = dict( + # Add new notification settings here. + enable_followed_topic_desktop_notifications=bool, + enable_followed_topic_email_notifications=bool, + enable_followed_topic_push_notifications=bool, + enable_followed_topic_audible_notifications=bool, + enable_followed_topic_wildcard_mentions_notify=bool, + automatically_follow_topics_policy=int, + automatically_unmute_topics_in_muted_streams_policy=int, + automatically_follow_topics_where_mentioned=bool, + ) + + notification_setting_types = { + **notification_settings_legacy, + **modern_notification_settings, + } + + # Define the types of the various automatically managed properties + property_types = { + **display_settings_legacy, + **notification_setting_types, + **modern_settings, + } + + class Meta: + abstract = True + + @staticmethod + def emojiset_choices() -> List[Dict[str, str]]: + return [ + dict(key=emojiset[0], text=emojiset[1]) for emojiset in UserProfile.EMOJISET_CHOICES + ] + + +class RealmUserDefault(UserBaseSettings): + """This table stores realm-level default values for user preferences + like notification settings, used when creating a new user account. + """ + + realm = models.OneToOneField("zerver.Realm", on_delete=CASCADE) + + +class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings): + USERNAME_FIELD = "email" + MAX_NAME_LENGTH = 100 + MIN_NAME_LENGTH = 2 + API_KEY_LENGTH = 32 + NAME_INVALID_CHARS = ["*", "`", "\\", ">", '"', "@"] + + DEFAULT_BOT = 1 + """ + Incoming webhook bots are limited to only sending messages via webhooks. + Thus, it is less of a security risk to expose their API keys to third-party services, + since they can't be used to read messages. + """ + INCOMING_WEBHOOK_BOT = 2 + # This value is also being used in web/src/settings_bots.js. + # On updating it here, update it there as well. + OUTGOING_WEBHOOK_BOT = 3 + """ + Embedded bots run within the Zulip server itself; events are added to the + embedded_bots queue and then handled by a QueueProcessingWorker. + """ + EMBEDDED_BOT = 4 + + BOT_TYPES = { + DEFAULT_BOT: "Generic bot", + INCOMING_WEBHOOK_BOT: "Incoming webhook", + OUTGOING_WEBHOOK_BOT: "Outgoing webhook", + EMBEDDED_BOT: "Embedded bot", + } + + SERVICE_BOT_TYPES = [ + OUTGOING_WEBHOOK_BOT, + EMBEDDED_BOT, + ] + + # For historical reasons, Zulip has two email fields. The + # `delivery_email` field is the user's email address, where all + # email notifications will be sent, and is used for all + # authentication use cases. + # + # The `email` field is the same as delivery_email in organizations + # with EMAIL_ADDRESS_VISIBILITY_EVERYONE. For other + # organizations, it will be a unique value of the form + # user1234@example.com. This field exists for backwards + # compatibility in Zulip APIs where users are referred to by their + # email address, not their ID; it should be used in all API use cases. + # + # Both fields are unique within a realm (in a case-insensitive + # fashion). Since Django's unique_together is case sensitive, this + # is enforced via SQL indexes created by + # zerver/migrations/0295_case_insensitive_email_indexes.py. + delivery_email = models.EmailField(blank=False, db_index=True) + email = models.EmailField(blank=False, db_index=True) + + realm = models.ForeignKey("zerver.Realm", on_delete=CASCADE) + # Foreign key to the Recipient object for PERSONAL type messages to this user. + recipient = models.ForeignKey("zerver.Recipient", null=True, on_delete=models.SET_NULL) + + INACCESSIBLE_USER_NAME = gettext_lazy("Unknown user") + # The user's name. We prefer the model of a full_name + # over first+last because cultures vary on how many + # names one has, whether the family name is first or last, etc. + # It also allows organizations to encode a bit of non-name data in + # the "name" attribute if desired, like gender pronouns, + # graduation year, etc. + full_name = models.CharField(max_length=MAX_NAME_LENGTH) + + date_joined = models.DateTimeField(default=timezone_now) + + # Terms of Service version number that this user has accepted. We + # use the special value TOS_VERSION_BEFORE_FIRST_LOGIN for users + # whose account was created without direct user interaction (via + # the API or a data import), and null for users whose account is + # fully created on servers that do not have a configured ToS. + TOS_VERSION_BEFORE_FIRST_LOGIN = "-1" + tos_version = models.CharField(null=True, max_length=10) + api_key = models.CharField(max_length=API_KEY_LENGTH, default=generate_api_key, unique=True) + + # A UUID generated on user creation. Introduced primarily to + # provide a unique key for a user for the mobile push + # notifications bouncer that will not have collisions after doing + # a data export and then import. + uuid = models.UUIDField(default=uuid4, unique=True) + + # Whether the user has access to server-level administrator pages, like /activity + is_staff = models.BooleanField(default=False) + + # For a normal user, this is True unless the user or an admin has + # deactivated their account. The name comes from Django; this field + # isn't related to presence or to whether the user has recently used Zulip. + # + # See also `long_term_idle`. + is_active = models.BooleanField(default=True, db_index=True) + + is_billing_admin = models.BooleanField(default=False, db_index=True) + + is_bot = models.BooleanField(default=False, db_index=True) + bot_type = models.PositiveSmallIntegerField(null=True, db_index=True) + bot_owner = models.ForeignKey("self", null=True, on_delete=models.SET_NULL) + + # Each role has a superset of the permissions of the next higher + # numbered role. When adding new roles, leave enough space for + # future roles to be inserted between currently adjacent + # roles. These constants appear in RealmAuditLog.extra_data, so + # changes to them will require a migration of RealmAuditLog. + ROLE_REALM_OWNER = 100 + ROLE_REALM_ADMINISTRATOR = 200 + ROLE_MODERATOR = 300 + ROLE_MEMBER = 400 + ROLE_GUEST = 600 + role = models.PositiveSmallIntegerField(default=ROLE_MEMBER, db_index=True) + + ROLE_TYPES = [ + ROLE_REALM_OWNER, + ROLE_REALM_ADMINISTRATOR, + ROLE_MODERATOR, + ROLE_MEMBER, + ROLE_GUEST, + ] + + # Whether the user has been "soft-deactivated" due to weeks of inactivity. + # For these users we avoid doing UserMessage table work, as an optimization + # for large Zulip organizations with lots of single-visit users. + long_term_idle = models.BooleanField(default=False, db_index=True) + + # When we last added basic UserMessage rows for a long_term_idle user. + last_active_message_id = models.IntegerField(null=True) + + # Mirror dummies are fake (!is_active) users used to provide + # message senders in our cross-protocol Zephyr<->Zulip content + # mirroring integration, so that we can display mirrored content + # like native Zulip messages (with a name + avatar, etc.). + is_mirror_dummy = models.BooleanField(default=False) + + # Users with this flag set are allowed to forge messages as sent by another + # user and to send to private streams; also used for Zephyr/Jabber mirroring. + can_forge_sender = models.BooleanField(default=False, db_index=True) + # Users with this flag set can create other users via API. + can_create_users = models.BooleanField(default=False, db_index=True) + + # Used for rate-limiting certain automated messages generated by bots + last_reminder = models.DateTimeField(default=None, null=True) + + # Minutes to wait before warning a bot owner that their bot sent a message + # to a nonexistent stream + BOT_OWNER_STREAM_ALERT_WAITPERIOD = 1 + + # API rate limits, formatted as a comma-separated list of range:max pairs + rate_limits = models.CharField(default="", max_length=100) + + # Default streams for some deprecated/legacy classes of bot users. + default_sending_stream = models.ForeignKey( + "zerver.Stream", + null=True, + related_name="+", + on_delete=models.SET_NULL, + ) + default_events_register_stream = models.ForeignKey( + "zerver.Stream", + null=True, + related_name="+", + on_delete=models.SET_NULL, + ) + default_all_public_streams = models.BooleanField(default=False) + + # A time zone name from the `tzdata` database, as found in zoneinfo.available_timezones(). + # + # The longest existing name is 32 characters long, so max_length=40 seems + # like a safe choice. + # + # In Django, the convention is to use an empty string instead of NULL/None + # for text-based fields. For more information, see + # https://docs.djangoproject.com/en/3.2/ref/models/fields/#django.db.models.Field.null. + timezone = models.CharField(max_length=40, default="") + + AVATAR_FROM_GRAVATAR = "G" + AVATAR_FROM_USER = "U" + AVATAR_SOURCES = ( + (AVATAR_FROM_GRAVATAR, "Hosted by Gravatar"), + (AVATAR_FROM_USER, "Uploaded by user"), + ) + avatar_source = models.CharField( + default=AVATAR_FROM_GRAVATAR, choices=AVATAR_SOURCES, max_length=1 + ) + avatar_version = models.PositiveSmallIntegerField(default=1) + avatar_hash = models.CharField(null=True, max_length=64) + + # TODO: TUTORIAL_STATUS was originally an optimization designed to + # allow us to skip querying the OnboardingStep table when loading + # /. This optimization is no longer effective, so it's possible we + # should delete it. + TUTORIAL_WAITING = "W" + TUTORIAL_STARTED = "S" + TUTORIAL_FINISHED = "F" + TUTORIAL_STATES = ( + (TUTORIAL_WAITING, "Waiting"), + (TUTORIAL_STARTED, "Started"), + (TUTORIAL_FINISHED, "Finished"), + ) + tutorial_status = models.CharField( + default=TUTORIAL_WAITING, choices=TUTORIAL_STATES, max_length=1 + ) + + # Contains serialized JSON of the form: + # [("step 1", true), ("step 2", false)] + # where the second element of each tuple is if the step has been + # completed. + onboarding_steps = models.TextField(default="[]") + + zoom_token = models.JSONField(default=None, null=True) + + objects = UserManager() + + ROLE_ID_TO_NAME_MAP = { + ROLE_REALM_OWNER: gettext_lazy("Organization owner"), + ROLE_REALM_ADMINISTRATOR: gettext_lazy("Organization administrator"), + ROLE_MODERATOR: gettext_lazy("Moderator"), + ROLE_MEMBER: gettext_lazy("Member"), + ROLE_GUEST: gettext_lazy("Guest"), + } + + def get_role_name(self) -> str: + return str(self.ROLE_ID_TO_NAME_MAP[self.role]) + + def profile_data(self) -> ProfileData: + from zerver.models import CustomProfileFieldValue, custom_profile_fields_for_realm + + values = CustomProfileFieldValue.objects.filter(user_profile=self) + user_data = { + v.field_id: {"value": v.value, "rendered_value": v.rendered_value} for v in values + } + data: ProfileData = [] + for field in custom_profile_fields_for_realm(self.realm_id): + field_values = user_data.get(field.id, None) + if field_values: + value, rendered_value = field_values.get("value"), field_values.get( + "rendered_value" + ) + else: + value, rendered_value = None, None + field_type = field.field_type + if value is not None: + converter = field.FIELD_CONVERTERS[field_type] + value = converter(value) + + field_data = field.as_dict() + data.append( + { + "id": field_data["id"], + "name": field_data["name"], + "type": field_data["type"], + "hint": field_data["hint"], + "field_data": field_data["field_data"], + "order": field_data["order"], + "value": value, + "rendered_value": rendered_value, + } + ) + + return data + + def can_admin_user(self, target_user: "UserProfile") -> bool: + """Returns whether this user has permission to modify target_user""" + if target_user.bot_owner_id == self.id: + return True + elif self.is_realm_admin and self.realm == target_user.realm: + return True + else: + return False + + @override + def __str__(self) -> str: + return f"{self.email} {self.realm!r}" + + @property + def is_provisional_member(self) -> bool: + if self.is_moderator: + return False + diff = (timezone_now() - self.date_joined).days + if diff < self.realm.waiting_period_threshold: + return True + return False + + @property + def is_realm_admin(self) -> bool: + return self.role in (UserProfile.ROLE_REALM_ADMINISTRATOR, UserProfile.ROLE_REALM_OWNER) + + @is_realm_admin.setter + def is_realm_admin(self, value: bool) -> None: + if value: + self.role = UserProfile.ROLE_REALM_ADMINISTRATOR + elif self.role == UserProfile.ROLE_REALM_ADMINISTRATOR: + # We need to be careful to not accidentally change + # ROLE_GUEST to ROLE_MEMBER here. + self.role = UserProfile.ROLE_MEMBER + + @property + def has_billing_access(self) -> bool: + return self.is_realm_owner or self.is_billing_admin + + @property + def is_realm_owner(self) -> bool: + return self.role == UserProfile.ROLE_REALM_OWNER + + @is_realm_owner.setter + def is_realm_owner(self, value: bool) -> None: + if value: + self.role = UserProfile.ROLE_REALM_OWNER + elif self.role == UserProfile.ROLE_REALM_OWNER: + # We need to be careful to not accidentally change + # ROLE_GUEST to ROLE_MEMBER here. + self.role = UserProfile.ROLE_MEMBER + + @property + def is_guest(self) -> bool: + return self.role == UserProfile.ROLE_GUEST + + @is_guest.setter + def is_guest(self, value: bool) -> None: + if value: + self.role = UserProfile.ROLE_GUEST + elif self.role == UserProfile.ROLE_GUEST: + # We need to be careful to not accidentally change + # ROLE_REALM_ADMINISTRATOR to ROLE_MEMBER here. + self.role = UserProfile.ROLE_MEMBER + + @property + def is_moderator(self) -> bool: + return self.role == UserProfile.ROLE_MODERATOR + + @is_moderator.setter + def is_moderator(self, value: bool) -> None: + if value: + self.role = UserProfile.ROLE_MODERATOR + elif self.role == UserProfile.ROLE_MODERATOR: + # We need to be careful to not accidentally change + # ROLE_GUEST to ROLE_MEMBER here. + self.role = UserProfile.ROLE_MEMBER + + @property + def is_incoming_webhook(self) -> bool: + return self.bot_type == UserProfile.INCOMING_WEBHOOK_BOT + + @property + def allowed_bot_types(self) -> List[int]: + from zerver.models import Realm + + allowed_bot_types = [] + if ( + self.is_realm_admin + or self.realm.bot_creation_policy != Realm.BOT_CREATION_LIMIT_GENERIC_BOTS + ): + allowed_bot_types.append(UserProfile.DEFAULT_BOT) + allowed_bot_types += [ + UserProfile.INCOMING_WEBHOOK_BOT, + UserProfile.OUTGOING_WEBHOOK_BOT, + ] + if settings.EMBEDDED_BOTS_ENABLED: + allowed_bot_types.append(UserProfile.EMBEDDED_BOT) + return allowed_bot_types + + def email_address_is_realm_public(self) -> bool: + # Bots always have EMAIL_ADDRESS_VISIBILITY_EVERYONE. + if self.email_address_visibility == UserProfile.EMAIL_ADDRESS_VISIBILITY_EVERYONE: + return True + return False + + def has_permission(self, policy_name: str) -> bool: + from zerver.lib.user_groups import is_user_in_group + from zerver.models import Realm + + if policy_name not in [ + "add_custom_emoji_policy", + "create_multiuse_invite_group", + "create_private_stream_policy", + "create_public_stream_policy", + "create_web_public_stream_policy", + "delete_own_message_policy", + "edit_topic_policy", + "invite_to_stream_policy", + "invite_to_realm_policy", + "move_messages_between_streams_policy", + "user_group_edit_policy", + ]: + raise AssertionError("Invalid policy") + + if policy_name in Realm.REALM_PERMISSION_GROUP_SETTINGS: + allowed_user_group = getattr(self.realm, policy_name) + return is_user_in_group(allowed_user_group, self) + + policy_value = getattr(self.realm, policy_name) + if policy_value == Realm.POLICY_NOBODY: + return False + + if policy_value == Realm.POLICY_EVERYONE: + return True + + if self.is_realm_owner: + return True + + if policy_value == Realm.POLICY_OWNERS_ONLY: + return False + + if self.is_realm_admin: + return True + + if policy_value == Realm.POLICY_ADMINS_ONLY: + return False + + if self.is_moderator: + return True + + if policy_value == Realm.POLICY_MODERATORS_ONLY: + return False + + if self.is_guest: + return False + + if policy_value == Realm.POLICY_MEMBERS_ONLY: + return True + + assert policy_value == Realm.POLICY_FULL_MEMBERS_ONLY + return not self.is_provisional_member + + def can_create_public_streams(self) -> bool: + return self.has_permission("create_public_stream_policy") + + def can_create_private_streams(self) -> bool: + return self.has_permission("create_private_stream_policy") + + def can_create_web_public_streams(self) -> bool: + if not self.realm.web_public_streams_enabled(): + return False + return self.has_permission("create_web_public_stream_policy") + + def can_subscribe_other_users(self) -> bool: + return self.has_permission("invite_to_stream_policy") + + def can_invite_users_by_email(self) -> bool: + return self.has_permission("invite_to_realm_policy") + + def can_create_multiuse_invite_to_realm(self) -> bool: + return self.has_permission("create_multiuse_invite_group") + + def can_move_messages_between_streams(self) -> bool: + return self.has_permission("move_messages_between_streams_policy") + + def can_edit_user_groups(self) -> bool: + return self.has_permission("user_group_edit_policy") + + def can_move_messages_to_another_topic(self) -> bool: + return self.has_permission("edit_topic_policy") + + def can_add_custom_emoji(self) -> bool: + return self.has_permission("add_custom_emoji_policy") + + def can_delete_own_message(self) -> bool: + return self.has_permission("delete_own_message_policy") + + def can_access_public_streams(self) -> bool: + return not (self.is_guest or self.realm.is_zephyr_mirror_realm) + + def major_tos_version(self) -> int: + if self.tos_version is not None: + return int(self.tos_version.split(".")[0]) + else: + return -1 + + def format_requester_for_logs(self) -> str: + return "{}@{}".format(self.id, self.realm.string_id or "root") + + @override + def set_password(self, password: Optional[str]) -> None: + if password is None: + self.set_unusable_password() + return + + from zproject.backends import check_password_strength + + if not check_password_strength(password): + raise PasswordTooWeakError + + super().set_password(password) + + class Meta: + indexes = [ + models.Index(Upper("email"), name="upper_userprofile_email_idx"), + ] + + +class PasswordTooWeakError(Exception): + pass + + +def remote_user_to_email(remote_user: str) -> str: + if settings.SSO_APPEND_DOMAIN is not None: + return Address(username=remote_user, domain=settings.SSO_APPEND_DOMAIN).addr_spec + return remote_user + + +# Make sure we flush the UserProfile object from our remote cache +# whenever we save it. +post_save.connect(flush_user_profile, sender=UserProfile) + + +@cache_with_key(user_profile_by_id_cache_key, timeout=3600 * 24 * 7) +def get_user_profile_by_id(user_profile_id: int) -> UserProfile: + return UserProfile.objects.select_related( + "realm", "realm__can_access_all_users_group", "bot_owner" + ).get(id=user_profile_id) + + +def get_user_profile_by_email(email: str) -> UserProfile: + """This function is intended to be used for + manual manage.py shell work; robust code must use get_user or + get_user_by_delivery_email instead, because Zulip supports + multiple users with a given (delivery) email address existing on a + single server (in different realms). + """ + return UserProfile.objects.select_related("realm").get(delivery_email__iexact=email.strip()) + + +@cache_with_key(user_profile_by_api_key_cache_key, timeout=3600 * 24 * 7) +def maybe_get_user_profile_by_api_key(api_key: str) -> Optional[UserProfile]: + try: + return UserProfile.objects.select_related( + "realm", "realm__can_access_all_users_group", "bot_owner" + ).get(api_key=api_key) + except UserProfile.DoesNotExist: + # We will cache failed lookups with None. The + # use case here is that broken API clients may + # continually ask for the same wrong API key, and + # we want to handle that as quickly as possible. + return None + + +def get_user_profile_by_api_key(api_key: str) -> UserProfile: + user_profile = maybe_get_user_profile_by_api_key(api_key) + if user_profile is None: + raise UserProfile.DoesNotExist + + return user_profile + + +def get_user_by_delivery_email(email: str, realm: "Realm") -> UserProfile: + """Fetches a user given their delivery email. For use in + authentication/registration contexts. Do not use for user-facing + views (e.g. Zulip API endpoints) as doing so would violate the + EMAIL_ADDRESS_VISIBILITY_ADMINS security model. Use get_user in + those code paths. + """ + return UserProfile.objects.select_related( + "realm", "realm__can_access_all_users_group", "bot_owner" + ).get(delivery_email__iexact=email.strip(), realm=realm) + + +def get_users_by_delivery_email(emails: Set[str], realm: "Realm") -> QuerySet[UserProfile]: + """This is similar to get_user_by_delivery_email, and + it has the same security caveats. It gets multiple + users and returns a QuerySet, since most callers + will only need two or three fields. + + If you are using this to get large UserProfile objects, you are + probably making a mistake, but if you must, + then use `select_related`. + """ + + """ + Django doesn't support delivery_email__iexact__in, so + we simply OR all the filters that we'd do for the + one-email case. + """ + email_filter = Q() + for email in emails: + email_filter |= Q(delivery_email__iexact=email.strip()) + + return UserProfile.objects.filter(realm=realm).filter(email_filter) + + +@cache_with_key(user_profile_cache_key, timeout=3600 * 24 * 7) +def get_user(email: str, realm: "Realm") -> UserProfile: + """Fetches the user by its visible-to-other users username (in the + `email` field). For use in API contexts; do not use in + authentication/registration contexts as doing so will break + authentication in organizations using + EMAIL_ADDRESS_VISIBILITY_ADMINS. In those code paths, use + get_user_by_delivery_email. + """ + return UserProfile.objects.select_related( + "realm", "realm__can_access_all_users_group", "bot_owner" + ).get(email__iexact=email.strip(), realm=realm) + + +def get_active_user(email: str, realm: "Realm") -> UserProfile: + """Variant of get_user_by_email that excludes deactivated users. + See get_user docstring for important usage notes.""" + user_profile = get_user(email, realm) + if not user_profile.is_active: + raise UserProfile.DoesNotExist + return user_profile + + +def get_user_profile_by_id_in_realm(uid: int, realm: "Realm") -> UserProfile: + return UserProfile.objects.select_related( + "realm", "realm__can_access_all_users_group", "bot_owner" + ).get(id=uid, realm=realm) + + +def get_active_user_profile_by_id_in_realm(uid: int, realm: "Realm") -> UserProfile: + user_profile = get_user_profile_by_id_in_realm(uid, realm) + if not user_profile.is_active: + raise UserProfile.DoesNotExist + return user_profile + + +def get_user_including_cross_realm(email: str, realm: "Realm") -> UserProfile: + if is_cross_realm_bot_email(email): + return get_system_bot(email, realm.id) + assert realm is not None + return get_user(email, realm) + + +@cache_with_key(bot_profile_cache_key, timeout=3600 * 24 * 7) +def get_system_bot(email: str, realm_id: int) -> UserProfile: + """ + This function doesn't use the realm_id argument yet, but requires + passing it as preparation for adding system bots to each realm instead + of having them all in a separate system bot realm. + If you're calling this function, use the id of the realm in which the system + bot will be after that migration. If the bot is supposed to send a message, + the same realm as the one *to* which the message will be sent should be used - because + cross-realm messages will be eliminated as part of the migration. + """ + return UserProfile.objects.select_related("realm").get(email__iexact=email.strip()) + + +def get_user_by_id_in_realm_including_cross_realm( + uid: int, + realm: Optional["Realm"], +) -> UserProfile: + user_profile = get_user_profile_by_id(uid) + if user_profile.realm == realm: + return user_profile + + # Note: This doesn't validate whether the `realm` passed in is + # None/invalid for the is_cross_realm_bot_email case. + if is_cross_realm_bot_email(user_profile.delivery_email): + return user_profile + + raise UserProfile.DoesNotExist + + +@cache_with_key(realm_user_dicts_cache_key, timeout=3600 * 24 * 7) +def get_realm_user_dicts(realm_id: int) -> List[RawUserDict]: + return list( + UserProfile.objects.filter( + realm_id=realm_id, + ).values(*realm_user_dict_fields) + ) + + +@cache_with_key(active_user_ids_cache_key, timeout=3600 * 24 * 7) +def active_user_ids(realm_id: int) -> List[int]: + query = UserProfile.objects.filter( + realm_id=realm_id, + is_active=True, + ).values_list("id", flat=True) + return list(query) + + +@cache_with_key(active_non_guest_user_ids_cache_key, timeout=3600 * 24 * 7) +def active_non_guest_user_ids(realm_id: int) -> List[int]: + query = ( + UserProfile.objects.filter( + realm_id=realm_id, + is_active=True, + ) + .exclude( + role=UserProfile.ROLE_GUEST, + ) + .values_list("id", flat=True) + ) + return list(query) + + +def bot_owner_user_ids(user_profile: UserProfile) -> Set[int]: + is_private_bot = ( + user_profile.default_sending_stream + and user_profile.default_sending_stream.invite_only + or user_profile.default_events_register_stream + and user_profile.default_events_register_stream.invite_only + ) + assert user_profile.bot_owner_id is not None + if is_private_bot: + return {user_profile.bot_owner_id} + else: + users = {user.id for user in user_profile.realm.get_human_admin_users()} + users.add(user_profile.bot_owner_id) + return users + + +def get_source_profile(email: str, realm_id: int) -> Optional[UserProfile]: + from zerver.models import Realm, get_realm_by_id + + try: + return get_user_by_delivery_email(email, get_realm_by_id(realm_id)) + except (Realm.DoesNotExist, UserProfile.DoesNotExist): + return None + + +@cache_with_key(lambda realm: bot_dicts_in_realm_cache_key(realm.id), timeout=3600 * 24 * 7) +def get_bot_dicts_in_realm(realm: "Realm") -> List[Dict[str, Any]]: + return list(UserProfile.objects.filter(realm=realm, is_bot=True).values(*bot_dict_fields)) + + +def is_cross_realm_bot_email(email: str) -> bool: + return email.lower() in settings.CROSS_REALM_BOT_EMAILS diff --git a/zerver/openapi/curl_param_value_generators.py b/zerver/openapi/curl_param_value_generators.py index 168c883d39..58e56d90c3 100644 --- a/zerver/openapi/curl_param_value_generators.py +++ b/zerver/openapi/curl_param_value_generators.py @@ -20,7 +20,8 @@ from zerver.lib.initial_password import initial_password from zerver.lib.test_classes import ZulipTestCase from zerver.lib.upload import upload_message_attachment from zerver.lib.users import get_api_key -from zerver.models import Client, Message, UserGroup, UserPresence, get_realm, get_user +from zerver.models import Client, Message, UserGroup, UserPresence, get_realm +from zerver.models.users import get_user GENERATOR_FUNCTIONS: Dict[str, Callable[[], Dict[str, object]]] = {} REGISTERED_GENERATOR_FUNCTIONS: Set[str] = set() diff --git a/zerver/openapi/python_examples.py b/zerver/openapi/python_examples.py index dda66ed850..2a1c6f4076 100644 --- a/zerver/openapi/python_examples.py +++ b/zerver/openapi/python_examples.py @@ -22,7 +22,8 @@ from typing import Any, Callable, Dict, List, Set, TypeVar from typing_extensions import ParamSpec from zulip import Client -from zerver.models import get_realm, get_user +from zerver.models import get_realm +from zerver.models.users import get_user from zerver.openapi.openapi import validate_against_openapi_schema ZULIP_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/zerver/tests/test_auth_backends.py b/zerver/tests/test_auth_backends.py index f2dda1ab77..f91ff9fcba 100644 --- a/zerver/tests/test_auth_backends.py +++ b/zerver/tests/test_auth_backends.py @@ -110,7 +110,6 @@ from zerver.models import ( CustomProfileField, CustomProfileFieldValue, MultiuseInvite, - PasswordTooWeakError, PreregistrationUser, Realm, RealmDomain, @@ -119,8 +118,8 @@ from zerver.models import ( UserProfile, clear_supported_auth_backends_cache, get_realm, - get_user_by_delivery_email, ) +from zerver.models.users import PasswordTooWeakError, get_user_by_delivery_email from zerver.signals import JUST_CREATED_THRESHOLD from zerver.views.auth import log_into_subdomain, maybe_send_to_registration from zproject.backends import ( diff --git a/zerver/tests/test_bots.py b/zerver/tests/test_bots.py index 06a65616cc..5567b76644 100644 --- a/zerver/tests/test_bots.py +++ b/zerver/tests/test_bots.py @@ -26,9 +26,8 @@ from zerver.models import ( get_bot_services, get_realm, get_stream, - get_user, - is_cross_realm_bot_email, ) +from zerver.models.users import get_user, is_cross_realm_bot_email # A test validator diff --git a/zerver/tests/test_cache.py b/zerver/tests/test_cache.py index f1743cd054..b7f75bf7ef 100644 --- a/zerver/tests/test_cache.py +++ b/zerver/tests/test_cache.py @@ -21,7 +21,8 @@ from zerver.lib.cache import ( validate_cache_key, ) from zerver.lib.test_classes import ZulipTestCase -from zerver.models import UserProfile, get_realm, get_system_bot, get_user, get_user_profile_by_id +from zerver.models import UserProfile, get_realm +from zerver.models.users import get_system_bot, get_user, get_user_profile_by_id class AppsTest(ZulipTestCase): diff --git a/zerver/tests/test_decorators.py b/zerver/tests/test_decorators.py index af666bed0b..4914d7121c 100644 --- a/zerver/tests/test_decorators.py +++ b/zerver/tests/test_decorators.py @@ -52,7 +52,8 @@ from zerver.lib.user_agent import parse_user_agent from zerver.lib.users import get_api_key from zerver.lib.utils import generate_api_key, has_api_key_format from zerver.middleware import LogRequests, parse_client -from zerver.models import Client, Realm, UserProfile, clear_client_cache, get_realm, get_user +from zerver.models import Client, Realm, UserProfile, clear_client_cache, get_realm +from zerver.models.users import get_user if settings.ZILENCER_ENABLED: from zilencer.models import RemoteZulipServer diff --git a/zerver/tests/test_email_change.py b/zerver/tests/test_email_change.py index 14d4dff518..0d8f7b61c7 100644 --- a/zerver/tests/test_email_change.py +++ b/zerver/tests/test_email_change.py @@ -18,14 +18,8 @@ from zerver.actions.realm_settings import do_deactivate_realm, do_set_realm_prop from zerver.actions.user_settings import do_change_user_setting, do_start_email_change_process from zerver.actions.users import do_deactivate_user from zerver.lib.test_classes import ZulipTestCase -from zerver.models import ( - EmailChangeStatus, - UserProfile, - get_realm, - get_user, - get_user_by_delivery_email, - get_user_profile_by_id, -) +from zerver.models import EmailChangeStatus, UserProfile, get_realm +from zerver.models.users import get_user, get_user_by_delivery_email, get_user_profile_by_id class EmailChangeTestCase(ZulipTestCase): diff --git a/zerver/tests/test_email_mirror.py b/zerver/tests/test_email_mirror.py index abe7a61e97..eea87920cd 100644 --- a/zerver/tests/test_email_mirror.py +++ b/zerver/tests/test_email_mirror.py @@ -37,15 +37,8 @@ from zerver.lib.send_email import FromAddress from zerver.lib.streams import ensure_stream from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import mock_queue_publish, most_recent_message, most_recent_usermessage -from zerver.models import ( - Attachment, - Recipient, - Stream, - UserProfile, - get_realm, - get_stream, - get_system_bot, -) +from zerver.models import Attachment, Recipient, Stream, UserProfile, get_realm, get_stream +from zerver.models.users import get_system_bot from zerver.worker.queue_processors import MirrorWorker if TYPE_CHECKING: diff --git a/zerver/tests/test_embedded_bot_system.py b/zerver/tests/test_embedded_bot_system.py index 1495692ba6..bd52d952df 100644 --- a/zerver/tests/test_embedded_bot_system.py +++ b/zerver/tests/test_embedded_bot_system.py @@ -5,13 +5,8 @@ from typing_extensions import override from zerver.lib.bot_lib import EmbeddedBotQuitError from zerver.lib.test_classes import ZulipTestCase -from zerver.models import ( - UserProfile, - get_display_recipient, - get_realm, - get_service_profile, - get_user, -) +from zerver.models import UserProfile, get_display_recipient, get_realm, get_service_profile +from zerver.models.users import get_user class TestEmbeddedBotMessaging(ZulipTestCase): diff --git a/zerver/tests/test_event_system.py b/zerver/tests/test_event_system.py index dcb879ac6c..0b316d679a 100644 --- a/zerver/tests/test_event_system.py +++ b/zerver/tests/test_event_system.py @@ -36,8 +36,8 @@ from zerver.models import ( get_client, get_realm, get_stream, - get_system_bot, ) +from zerver.models.users import get_system_bot from zerver.tornado.event_queue import ( allocate_client_descriptor, clear_client_event_queues_for_testing, diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index af9f3abaef..6c02651b29 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -243,8 +243,8 @@ from zerver.models import ( UserTopic, get_client, get_stream, - get_user_by_delivery_email, ) +from zerver.models.users import get_user_by_delivery_email from zerver.openapi.openapi import validate_against_openapi_schema from zerver.tornado.django_api import send_event from zerver.tornado.event_queue import ( diff --git a/zerver/tests/test_example.py b/zerver/tests/test_example.py index 7387c003dd..4be0e57a84 100644 --- a/zerver/tests/test_example.py +++ b/zerver/tests/test_example.py @@ -11,7 +11,8 @@ from zerver.lib.streams import access_stream_for_send_message from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import most_recent_message from zerver.lib.users import is_administrator_role -from zerver.models import UserProfile, UserStatus, get_realm, get_stream, get_user_by_delivery_email +from zerver.models import UserProfile, UserStatus, get_realm, get_stream +from zerver.models.users import get_user_by_delivery_email # Most Zulip tests use ZulipTestCase, which inherits from django.test.TestCase. diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index 52e5f1482f..4a00c1b7de 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -32,9 +32,8 @@ from zerver.models import ( UserProfile, get_realm, get_stream, - get_system_bot, - get_user, ) +from zerver.models.users import get_system_bot, get_user from zerver.worker.queue_processors import UserActivityWorker if TYPE_CHECKING: diff --git a/zerver/tests/test_import_export.py b/zerver/tests/test_import_export.py index b36ecfdd29..a701a7b63d 100644 --- a/zerver/tests/test_import_export.py +++ b/zerver/tests/test_import_export.py @@ -89,9 +89,8 @@ from zerver.models import ( get_huddle_hash, get_realm, get_stream, - get_system_bot, - get_user_by_delivery_email, ) +from zerver.models.users import get_system_bot, get_user_by_delivery_email def make_datetime(val: float) -> datetime: diff --git a/zerver/tests/test_integrations_dev_panel.py b/zerver/tests/test_integrations_dev_panel.py index cf1ae3ba4b..5fd7d70858 100644 --- a/zerver/tests/test_integrations_dev_panel.py +++ b/zerver/tests/test_integrations_dev_panel.py @@ -4,7 +4,8 @@ import orjson from django.core.exceptions import ValidationError from zerver.lib.test_classes import ZulipTestCase -from zerver.models import Message, Stream, get_realm, get_user +from zerver.models import Message, Stream, get_realm +from zerver.models.users import get_user class TestIntegrationsDevPanel(ZulipTestCase): diff --git a/zerver/tests/test_invite.py b/zerver/tests/test_invite.py index f2c7e7c397..c13f925c58 100644 --- a/zerver/tests/test_invite.py +++ b/zerver/tests/test_invite.py @@ -66,8 +66,8 @@ from zerver.models import ( UserProfile, get_realm, get_stream, - get_user_by_delivery_email, ) +from zerver.models.users import get_user_by_delivery_email from zerver.views.invite import INVITATION_LINK_VALIDITY_MINUTES, get_invitee_emails_set from zerver.views.registration import accounts_home diff --git a/zerver/tests/test_management_commands.py b/zerver/tests/test_management_commands.py index 26bb8801b0..2391ec7aed 100644 --- a/zerver/tests/test_management_commands.py +++ b/zerver/tests/test_management_commands.py @@ -20,16 +20,8 @@ from zerver.actions.reactions import do_add_reaction from zerver.lib.management import ZulipBaseCommand, check_config from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import most_recent_message, stdout_suppressed -from zerver.models import ( - Message, - Reaction, - Realm, - Recipient, - UserProfile, - get_realm, - get_stream, - get_user_profile_by_email, -) +from zerver.models import Message, Reaction, Realm, Recipient, UserProfile, get_realm, get_stream +from zerver.models.users import get_user_profile_by_email class TestCheckConfig(ZulipTestCase): diff --git a/zerver/tests/test_mattermost_importer.py b/zerver/tests/test_mattermost_importer.py index 16a7ee83e8..281a668f86 100644 --- a/zerver/tests/test_mattermost_importer.py +++ b/zerver/tests/test_mattermost_importer.py @@ -28,7 +28,8 @@ from zerver.data_import.user_handler import UserHandler from zerver.lib.emoji import name_to_codepoint from zerver.lib.import_realm import do_import_realm from zerver.lib.test_classes import ZulipTestCase -from zerver.models import Message, Reaction, Recipient, UserProfile, get_realm, get_user +from zerver.models import Message, Reaction, Recipient, UserProfile, get_realm +from zerver.models.users import get_user class MatterMostImporter(ZulipTestCase): diff --git a/zerver/tests/test_message_send.py b/zerver/tests/test_message_send.py index 51d649ce96..525692fd4e 100644 --- a/zerver/tests/test_message_send.py +++ b/zerver/tests/test_message_send.py @@ -60,10 +60,9 @@ from zerver.models import ( get_or_create_huddle, get_realm, get_stream, - get_system_bot, - get_user, ) from zerver.models.constants import MAX_TOPIC_NAME_LENGTH +from zerver.models.users import get_system_bot, get_user from zerver.views.message_send import InvalidMirrorInputError diff --git a/zerver/tests/test_mirror_users.py b/zerver/tests/test_mirror_users.py index b310c4f80b..24464d1e1b 100644 --- a/zerver/tests/test_mirror_users.py +++ b/zerver/tests/test_mirror_users.py @@ -8,7 +8,8 @@ from zerver.actions.message_send import create_mirror_user_if_needed from zerver.lib.create_user import create_user_profile from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import reset_email_visibility_to_everyone_in_zulip_realm -from zerver.models import UserProfile, get_client, get_realm, get_user +from zerver.models import UserProfile, get_client, get_realm +from zerver.models.users import get_user from zerver.views.message_send import InvalidMirrorInputError, create_mirrored_message_users diff --git a/zerver/tests/test_outgoing_webhook_interfaces.py b/zerver/tests/test_outgoing_webhook_interfaces.py index e56e3d2e45..f66510cb0f 100644 --- a/zerver/tests/test_outgoing_webhook_interfaces.py +++ b/zerver/tests/test_outgoing_webhook_interfaces.py @@ -12,14 +12,8 @@ from zerver.lib.outgoing_webhook import get_service_interface_class, process_suc from zerver.lib.test_classes import ZulipTestCase from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.topic import TOPIC_NAME -from zerver.models import ( - SLACK_INTERFACE, - Message, - NotificationTriggers, - get_realm, - get_stream, - get_user, -) +from zerver.models import SLACK_INTERFACE, Message, NotificationTriggers, get_realm, get_stream +from zerver.models.users import get_user from zerver.openapi.openapi import validate_against_openapi_schema diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py index b5baf22be0..e989f4d0bf 100644 --- a/zerver/tests/test_realm.py +++ b/zerver/tests/test_realm.py @@ -56,9 +56,8 @@ from zerver.models import ( UserProfile, get_realm, get_stream, - get_system_bot, - get_user_profile_by_id, ) +from zerver.models.users import get_system_bot, get_user_profile_by_id class RealmTest(ZulipTestCase): diff --git a/zerver/tests/test_retention.py b/zerver/tests/test_retention.py index 6523897d37..340fee5689 100644 --- a/zerver/tests/test_retention.py +++ b/zerver/tests/test_retention.py @@ -40,8 +40,8 @@ from zerver.models import ( get_client, get_realm, get_stream, - get_system_bot, ) +from zerver.models.users import get_system_bot # Class with helper functions useful for testing archiving of reactions: from zerver.tornado.django_api import send_event diff --git a/zerver/tests/test_rocketchat_importer.py b/zerver/tests/test_rocketchat_importer.py index 7ea2717702..bc2b98bb15 100644 --- a/zerver/tests/test_rocketchat_importer.py +++ b/zerver/tests/test_rocketchat_importer.py @@ -28,7 +28,8 @@ from zerver.data_import.user_handler import UserHandler from zerver.lib.emoji import name_to_codepoint from zerver.lib.import_realm import do_import_realm from zerver.lib.test_classes import ZulipTestCase -from zerver.models import Message, Reaction, Recipient, UserProfile, get_realm, get_user +from zerver.models import Message, Reaction, Recipient, UserProfile, get_realm +from zerver.models.users import get_user class RocketChatImporter(ZulipTestCase): diff --git a/zerver/tests/test_settings.py b/zerver/tests/test_settings.py index 29b1fbef5b..a8c9a44b43 100644 --- a/zerver/tests/test_settings.py +++ b/zerver/tests/test_settings.py @@ -16,8 +16,8 @@ from zerver.models import ( NotificationTriggers, ScheduledMessageNotificationEmail, UserProfile, - get_user_profile_by_api_key, ) +from zerver.models.users import get_user_profile_by_api_key if TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py index 3426c29e80..f41e1d7ab6 100644 --- a/zerver/tests/test_signup.py +++ b/zerver/tests/test_signup.py @@ -78,10 +78,8 @@ from zerver.models import ( UserProfile, get_realm, get_stream, - get_system_bot, - get_user, - get_user_by_delivery_email, ) +from zerver.models.users import get_system_bot, get_user, get_user_by_delivery_email from zerver.views.auth import redirect_and_log_into_subdomain, start_two_factor_auth from zerver.views.development.registration import confirmation_key from zproject.backends import ExternalAuthDataDict, ExternalAuthResult, email_auth_enabled diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py index 16f64672ce..23bb63cedb 100644 --- a/zerver/tests/test_subs.py +++ b/zerver/tests/test_subs.py @@ -104,15 +104,13 @@ from zerver.models import ( UserGroup, UserMessage, UserProfile, - active_non_guest_user_ids, get_default_stream_groups, get_realm, get_stream, - get_user, - get_user_profile_by_id_in_realm, validate_attachment_request, validate_attachment_request_for_spectator_access, ) +from zerver.models.users import active_non_guest_user_ids, get_user, get_user_profile_by_id_in_realm from zerver.views.streams import compose_views if TYPE_CHECKING: diff --git a/zerver/tests/test_tutorial.py b/zerver/tests/test_tutorial.py index caa710e5eb..9d0fb360ac 100644 --- a/zerver/tests/test_tutorial.py +++ b/zerver/tests/test_tutorial.py @@ -4,7 +4,8 @@ from typing_extensions import override from zerver.actions.message_send import internal_send_private_message from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import message_stream_count, most_recent_message -from zerver.models import UserProfile, get_system_bot +from zerver.models import UserProfile +from zerver.models.users import get_system_bot class TutorialTests(ZulipTestCase): diff --git a/zerver/tests/test_upload.py b/zerver/tests/test_upload.py index 240129561a..115b81cd75 100644 --- a/zerver/tests/test_upload.py +++ b/zerver/tests/test_upload.py @@ -46,10 +46,9 @@ from zerver.models import ( RealmDomain, UserProfile, get_realm, - get_system_bot, - get_user_by_delivery_email, validate_attachment_request, ) +from zerver.models.users import get_system_bot, get_user_by_delivery_email class FileUploadTest(UploadSerializeMixin, ZulipTestCase): diff --git a/zerver/tests/test_upload_local.py b/zerver/tests/test_upload_local.py index ec4c194b85..368436c7fe 100644 --- a/zerver/tests/test_upload_local.py +++ b/zerver/tests/test_upload_local.py @@ -22,7 +22,8 @@ from zerver.lib.upload import ( ) from zerver.lib.upload.base import DEFAULT_EMOJI_SIZE, MEDIUM_AVATAR_SIZE, resize_avatar from zerver.lib.upload.local import write_local_file -from zerver.models import Attachment, RealmEmoji, get_realm, get_system_bot +from zerver.models import Attachment, RealmEmoji, get_realm +from zerver.models.users import get_system_bot class LocalStorageTest(UploadSerializeMixin, ZulipTestCase): diff --git a/zerver/tests/test_upload_s3.py b/zerver/tests/test_upload_s3.py index 02fcff0653..49716a0a6d 100644 --- a/zerver/tests/test_upload_s3.py +++ b/zerver/tests/test_upload_s3.py @@ -36,7 +36,8 @@ from zerver.lib.upload.base import ( resize_avatar, ) from zerver.lib.upload.s3 import S3UploadBackend -from zerver.models import Attachment, RealmEmoji, UserProfile, get_realm, get_system_bot +from zerver.models import Attachment, RealmEmoji, UserProfile, get_realm +from zerver.models.users import get_system_bot class S3Test(ZulipTestCase): diff --git a/zerver/tests/test_users.py b/zerver/tests/test_users.py index 3b1ccd12d3..d5e7272f96 100644 --- a/zerver/tests/test_users.py +++ b/zerver/tests/test_users.py @@ -79,8 +79,10 @@ from zerver.models import ( get_client, get_fake_email_domain, get_realm, - get_source_profile, get_stream, +) +from zerver.models.users import ( + get_source_profile, get_system_bot, get_user, get_user_by_delivery_email, diff --git a/zerver/tests/test_webhooks_common.py b/zerver/tests/test_webhooks_common.py index 358d67f52a..444bd0d0e4 100644 --- a/zerver/tests/test_webhooks_common.py +++ b/zerver/tests/test_webhooks_common.py @@ -21,7 +21,8 @@ from zerver.lib.webhooks.common import ( standardize_headers, validate_extract_webhook_http_header, ) -from zerver.models import UserProfile, get_realm, get_user +from zerver.models import UserProfile, get_realm +from zerver.models.users import get_user class WebhooksCommonTestCase(ZulipTestCase): diff --git a/zerver/tests/test_zephyr.py b/zerver/tests/test_zephyr.py index f4d04ca3ea..0721ce61ad 100644 --- a/zerver/tests/test_zephyr.py +++ b/zerver/tests/test_zephyr.py @@ -6,7 +6,8 @@ import orjson from zerver.lib.test_classes import ZulipTestCase from zerver.lib.users import get_api_key -from zerver.models import get_realm, get_user +from zerver.models import get_realm +from zerver.models.users import get_user if TYPE_CHECKING: from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse diff --git a/zerver/tornado/views.py b/zerver/tornado/views.py index ec5b7a711f..5b34aaa7a2 100644 --- a/zerver/tornado/views.py +++ b/zerver/tornado/views.py @@ -20,7 +20,8 @@ from zerver.lib.validator import ( check_string, to_non_negative_int, ) -from zerver.models import Client, UserProfile, get_client, get_user_profile_by_id +from zerver.models import Client, UserProfile, get_client +from zerver.models.users import get_user_profile_by_id from zerver.tornado.descriptors import is_current_port from zerver.tornado.event_queue import access_client_descriptor, fetch_events, process_notification from zerver.tornado.sharding import get_user_tornado_port, notify_tornado_queue_name diff --git a/zerver/views/auth.py b/zerver/views/auth.py index 093e91d821..6f57ba1ed5 100644 --- a/zerver/views/auth.py +++ b/zerver/views/auth.py @@ -76,8 +76,8 @@ from zerver.models import ( UserProfile, filter_to_valid_prereg_users, get_realm, - remote_user_to_email, ) +from zerver.models.users import remote_user_to_email from zerver.signals import email_on_new_login from zerver.views.errors import config_error from zproject.backends import ( diff --git a/zerver/views/development/email_log.py b/zerver/views/development/email_log.py index 4b4fe010cb..e359cecee2 100644 --- a/zerver/views/development/email_log.py +++ b/zerver/views/development/email_log.py @@ -16,7 +16,8 @@ from zerver.actions.users import change_user_is_active from zerver.lib.email_notifications import enqueue_welcome_emails, send_account_registered_email from zerver.lib.request import REQ, has_request_variables from zerver.lib.response import json_success -from zerver.models import Realm, get_realm, get_realm_stream, get_user_by_delivery_email +from zerver.models import Realm, get_realm, get_realm_stream +from zerver.models.users import get_user_by_delivery_email from zerver.views.invite import INVITATION_LINK_VALIDITY_MINUTES from zproject.email_backends import get_forward_address, set_forward_address diff --git a/zerver/views/message_send.py b/zerver/views/message_send.py index ca25345158..ef4e3d5ff0 100644 --- a/zerver/views/message_send.py +++ b/zerver/views/message_send.py @@ -22,7 +22,8 @@ from zerver.lib.topic import REQ_topic from zerver.lib.validator import check_bool, check_string_in, to_float from zerver.lib.zcommand import process_zcommands from zerver.lib.zephyr import compute_mit_user_fullname -from zerver.models import Client, Message, RealmDomain, UserProfile, get_user_including_cross_realm +from zerver.models import Client, Message, RealmDomain, UserProfile +from zerver.models.users import get_user_including_cross_realm class InvalidMirrorInputError(Exception): diff --git a/zerver/views/presence.py b/zerver/views/presence.py index 651152950d..56d5ab5035 100644 --- a/zerver/views/presence.py +++ b/zerver/views/presence.py @@ -19,14 +19,8 @@ from zerver.lib.response import json_success from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.typed_endpoint import ApiParamConfig, typed_endpoint from zerver.lib.users import check_can_access_user -from zerver.models import ( - UserActivity, - UserPresence, - UserProfile, - UserStatus, - get_active_user, - get_active_user_profile_by_id_in_realm, -) +from zerver.models import UserActivity, UserPresence, UserProfile, UserStatus +from zerver.models.users import get_active_user, get_active_user_profile_by_id_in_realm def get_presence_backend( diff --git a/zerver/views/registration.py b/zerver/views/registration.py index c6872e8455..6f25640037 100644 --- a/zerver/views/registration.py +++ b/zerver/views/registration.py @@ -86,11 +86,10 @@ from zerver.models import ( get_default_stream_groups, get_org_type_display_name, get_realm, - get_source_profile, - get_user_by_delivery_email, name_changes_disabled, ) from zerver.models.constants import MAX_LANGUAGE_ID_LENGTH +from zerver.models.users import get_source_profile, get_user_by_delivery_email from zerver.views.auth import ( create_preregistration_realm, create_preregistration_user, diff --git a/zerver/views/streams.py b/zerver/views/streams.py index 23e14774c9..64611e4ffb 100644 --- a/zerver/views/streams.py +++ b/zerver/views/streams.py @@ -99,7 +99,8 @@ from zerver.lib.validator import ( check_union, to_non_negative_int, ) -from zerver.models import Realm, Stream, UserGroup, UserMessage, UserProfile, get_system_bot +from zerver.models import Realm, Stream, UserGroup, UserMessage, UserProfile +from zerver.models.users import get_system_bot def principal_to_user_profile(agent: UserProfile, principal: Union[str, int]) -> UserProfile: diff --git a/zerver/views/user_groups.py b/zerver/views/user_groups.py index 91af45bcca..dd09d2be17 100644 --- a/zerver/views/user_groups.py +++ b/zerver/views/user_groups.py @@ -37,7 +37,8 @@ from zerver.lib.user_groups import ( ) from zerver.lib.users import access_user_by_id, user_ids_to_users from zerver.lib.validator import check_bool, check_int, check_list -from zerver.models import UserGroup, UserProfile, get_system_bot +from zerver.models import UserGroup, UserProfile +from zerver.models.users import get_system_bot from zerver.views.streams import compose_views diff --git a/zerver/views/users.py b/zerver/views/users.py index 4ef656222c..5f9647b281 100644 --- a/zerver/views/users.py +++ b/zerver/views/users.py @@ -94,6 +94,8 @@ from zerver.models import ( Service, Stream, UserProfile, +) +from zerver.models.users import ( get_user_by_delivery_email, get_user_by_id_in_realm_including_cross_realm, get_user_including_cross_realm, diff --git a/zerver/webhooks/dialogflow/view.py b/zerver/webhooks/dialogflow/view.py index 15b3ea07f6..054bb01a05 100644 --- a/zerver/webhooks/dialogflow/view.py +++ b/zerver/webhooks/dialogflow/view.py @@ -7,7 +7,8 @@ from zerver.lib.request import RequestNotes from zerver.lib.response import json_success from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint from zerver.lib.validator import WildValue, check_int, check_string -from zerver.models import UserProfile, get_user +from zerver.models import UserProfile +from zerver.models.users import get_user @webhook_view("Dialogflow") diff --git a/zerver/webhooks/helloworld/tests.py b/zerver/webhooks/helloworld/tests.py index ff2e8179b2..3ee3010f56 100644 --- a/zerver/webhooks/helloworld/tests.py +++ b/zerver/webhooks/helloworld/tests.py @@ -1,7 +1,8 @@ from django.conf import settings from zerver.lib.test_classes import WebhookTestCase -from zerver.models import get_realm, get_system_bot +from zerver.models import get_realm +from zerver.models.users import get_system_bot class HelloWorldHookTests(WebhookTestCase): diff --git a/zerver/webhooks/jira/view.py b/zerver/webhooks/jira/view.py index 7e864e22cf..08118a1f17 100644 --- a/zerver/webhooks/jira/view.py +++ b/zerver/webhooks/jira/view.py @@ -13,7 +13,8 @@ from zerver.lib.response import json_success from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint from zerver.lib.validator import WildValue, check_none_or, check_string from zerver.lib.webhooks.common import check_send_webhook_message -from zerver.models import Realm, UserProfile, get_user_by_delivery_email +from zerver.models import Realm, UserProfile +from zerver.models.users import get_user_by_delivery_email IGNORED_EVENTS = [ "attachment_created", diff --git a/zerver/webhooks/teamcity/tests.py b/zerver/webhooks/teamcity/tests.py index 34b62ed1fc..50ac0d8bf7 100644 --- a/zerver/webhooks/teamcity/tests.py +++ b/zerver/webhooks/teamcity/tests.py @@ -2,7 +2,8 @@ import orjson from zerver.lib.send_email import FromAddress from zerver.lib.test_classes import WebhookTestCase -from zerver.models import Recipient, get_realm, get_user_by_delivery_email +from zerver.models import Recipient, get_realm +from zerver.models.users import get_user_by_delivery_email from zerver.webhooks.teamcity.view import MISCONFIGURED_PAYLOAD_TYPE_ERROR_MESSAGE diff --git a/zerver/worker/queue_processors.py b/zerver/worker/queue_processors.py index 5e22561bb3..26effad74b 100644 --- a/zerver/worker/queue_processors.py +++ b/zerver/worker/queue_processors.py @@ -107,9 +107,8 @@ from zerver.models import ( filter_to_valid_prereg_users, get_bot_services, get_client, - get_system_bot, - get_user_profile_by_id, ) +from zerver.models.users import get_system_bot, get_user_profile_by_id logger = logging.getLogger(__name__) diff --git a/zilencer/management/commands/populate_db.py b/zilencer/management/commands/populate_db.py index 0c31bd7a98..eca2eefeb6 100644 --- a/zilencer/management/commands/populate_db.py +++ b/zilencer/management/commands/populate_db.py @@ -69,10 +69,8 @@ from zerver.models import ( get_or_create_huddle, get_realm, get_stream, - get_user, - get_user_by_delivery_email, - get_user_profile_by_id, ) +from zerver.models.users import get_user, get_user_by_delivery_email, get_user_profile_by_id from zilencer.models import RemoteRealm, RemoteZulipServer from zilencer.views import update_remote_realm_data_for_server diff --git a/zilencer/management/commands/sync_api_key.py b/zilencer/management/commands/sync_api_key.py index 9a03255faf..84458b2f47 100644 --- a/zilencer/management/commands/sync_api_key.py +++ b/zilencer/management/commands/sync_api_key.py @@ -5,7 +5,8 @@ from typing import Any from django.core.management.base import BaseCommand from typing_extensions import override -from zerver.models import UserProfile, get_realm, get_user_by_delivery_email +from zerver.models import UserProfile, get_realm +from zerver.models.users import get_user_by_delivery_email class Command(BaseCommand): diff --git a/zproject/backends.py b/zproject/backends.py index 2828daef1e..3a2fdd2fb5 100644 --- a/zproject/backends.py +++ b/zproject/backends.py @@ -106,7 +106,6 @@ from zerver.models import ( DisposableEmailError, DomainNotAllowedForRealmError, EmailContainsPlusError, - PasswordTooWeakError, PreregistrationRealm, PreregistrationUser, Realm, @@ -115,10 +114,13 @@ from zerver.models import ( UserProfile, custom_profile_fields_for_realm, get_realm, + supported_auth_backends, +) +from zerver.models.users import ( + PasswordTooWeakError, get_user_by_delivery_email, get_user_profile_by_id, remote_user_to_email, - supported_auth_backends, ) from zproject.settings_types import OIDCIdPConfigDict diff --git a/zproject/sentry.py b/zproject/sentry.py index ff8850ddbf..7d184b4306 100644 --- a/zproject/sentry.py +++ b/zproject/sentry.py @@ -24,7 +24,7 @@ def add_context(event: "Event", hint: "Hint") -> Optional["Event"]: return None from django.conf import settings - from zerver.models import get_user_profile_by_id + from zerver.models.users import get_user_profile_by_id with capture_internal_exceptions(): # event.user is the user context, from Sentry, which is