def test_canonicalize_timezone(self) -> None: self.assertEqual(canonicalize_timezone("America/Los_Angeles"), "America/Los_Angeles") self.assertEqual(canonicalize_timezone("US/Pacific"), "America/Los_Angeles") self.assertEqual(canonicalize_timezone("Gondor/Minas_Tirith"), "Gondor/Minas_Tirith")
def copy_default_settings(settings_source: Union[UserProfile, RealmUserDefault], target_profile: UserProfile) -> None: # Important note: Code run from here to configure the user's # settings should not call send_event, as that would cause clients # to throw an exception (we haven't sent the realm_user/add event # yet, so that event will include the updated details of target_profile). # # Note that this function will do at least one save() on target_profile. for settings_name in UserBaseSettings.property_types: if settings_name in ["default_language", "enable_login_emails"] and isinstance( settings_source, RealmUserDefault): continue value = getattr(settings_source, settings_name) setattr(target_profile, settings_name, value) if isinstance(settings_source, RealmUserDefault): target_profile.save() return setattr(target_profile, "full_name", settings_source.full_name) setattr(target_profile, "timezone", canonicalize_timezone(settings_source.timezone)) target_profile.save() if settings_source.avatar_source == UserProfile.AVATAR_FROM_USER: from zerver.lib.actions import do_change_avatar_fields do_change_avatar_fields( target_profile, UserProfile.AVATAR_FROM_USER, skip_notify=True, acting_user=target_profile, ) copy_avatar(settings_source, target_profile) copy_hotspots(settings_source, target_profile)
def format_user_row( realm: Realm, acting_user: Optional[UserProfile], row: Dict[str, Any], client_gravatar: bool, user_avatar_url_field_optional: bool, custom_profile_field_data: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """Formats a user row returned by a database fetch using .values(*realm_user_dict_fields) into a dictionary representation of that user for API delivery to clients. The acting_user argument is used for permissions checks. """ is_admin = is_administrator_role(row["role"]) is_owner = row["role"] == UserProfile.ROLE_REALM_OWNER is_guest = row["role"] == UserProfile.ROLE_GUEST is_bot = row["is_bot"] result = dict( email=row["email"], user_id=row["id"], avatar_version=row["avatar_version"], is_admin=is_admin, is_owner=is_owner, is_guest=is_guest, is_billing_admin=row["is_billing_admin"], role=row["role"], is_bot=is_bot, full_name=row["full_name"], timezone=canonicalize_timezone(row["timezone"]), is_active=row["is_active"], date_joined=row["date_joined"].isoformat(), ) # Zulip clients that support using `GET /avatar/{user_id}` as a # fallback if we didn't send an avatar URL in the user object pass # user_avatar_url_field_optional in client_capabilities. # # This is a major network performance optimization for # organizations with 10,000s of users where we would otherwise # send avatar URLs in the payload (either because most users have # uploaded avatars or because EMAIL_ADDRESS_VISIBILITY_ADMINS # prevents the older client_gravatar optimization from helping). # The performance impact is large largely because the hashes in # avatar URLs structurally cannot compress well. # # The user_avatar_url_field_optional gives the server sole # discretion in deciding for which users we want to send the # avatar URL (Which saves clients an RTT at the cost of some # bandwidth). At present, the server looks at `long_term_idle` to # decide which users to include avatars for, piggy-backing on a # different optimization for organizations with 10,000s of users. include_avatar_url = not user_avatar_url_field_optional or not row["long_term_idle"] if include_avatar_url: result["avatar_url"] = get_avatar_field( user_id=row["id"], realm_id=realm.id, email=row["delivery_email"], avatar_source=row["avatar_source"], avatar_version=row["avatar_version"], medium=False, client_gravatar=client_gravatar, ) if acting_user is not None and can_access_delivery_email(acting_user): result["delivery_email"] = row["delivery_email"] if is_bot: result["bot_type"] = row["bot_type"] if row["email"] in settings.CROSS_REALM_BOT_EMAILS: result["is_system_bot"] = True # Note that bot_owner_id can be None with legacy data. result["bot_owner_id"] = row["bot_owner_id"] elif custom_profile_field_data is not None: result["profile_data"] = custom_profile_field_data return result
def to_timezone_or_empty(var_name: str, s: str) -> str: if s in pytz.all_timezones_set: return canonicalize_timezone(s) else: return ""
def do_change_user_setting( user_profile: UserProfile, setting_name: str, setting_value: Union[bool, str, int], *, acting_user: Optional[UserProfile], ) -> None: old_value = getattr(user_profile, setting_name) event_time = timezone_now() if setting_name == "timezone": assert isinstance(setting_value, str) setting_value = canonicalize_timezone(setting_value) else: property_type = UserProfile.property_types[setting_name] assert isinstance(setting_value, property_type) setattr(user_profile, setting_name, setting_value) # TODO: Move these database actions into a transaction.atomic block. user_profile.save(update_fields=[setting_name]) if setting_name in UserProfile.notification_setting_types: # Prior to all personal settings being managed by property_types, # these were only created for notification settings. # # TODO: Start creating these for all settings, and do a # backfilled=True migration. RealmAuditLog.objects.create( realm=user_profile.realm, event_type=RealmAuditLog.USER_SETTING_CHANGED, event_time=event_time, acting_user=acting_user, modified_user=user_profile, extra_data=orjson.dumps({ RealmAuditLog.OLD_VALUE: old_value, RealmAuditLog.NEW_VALUE: setting_value, "property": setting_name, }).decode(), ) # Disabling digest emails should clear a user's email queue if setting_name == "enable_digest_emails" and not setting_value: clear_scheduled_emails(user_profile.id, ScheduledEmail.DIGEST) if setting_name == "email_notifications_batching_period_seconds": assert isinstance(old_value, int) assert isinstance(setting_value, int) update_scheduled_email_notifications_time(user_profile, old_value, setting_value) event = { "type": "user_settings", "op": "update", "property": setting_name, "value": setting_value, } if setting_name == "default_language": assert isinstance(setting_value, str) event["language_name"] = get_language_name(setting_value) transaction.on_commit( lambda: send_event(user_profile.realm, event, [user_profile.id])) if setting_name in UserProfile.notification_settings_legacy: # This legacy event format is for backwards-compatibility with # clients that don't support the new user_settings event type. # We only send this for settings added before Feature level 89. legacy_event = { "type": "update_global_notifications", "user": user_profile.email, "notification_name": setting_name, "setting": setting_value, } transaction.on_commit(lambda: send_event( user_profile.realm, legacy_event, [user_profile.id])) if setting_name in UserProfile.display_settings_legacy or setting_name == "timezone": # This legacy event format is for backwards-compatibility with # clients that don't support the new user_settings event type. # We only send this for settings added before Feature level 89. legacy_event = { "type": "update_display_settings", "user": user_profile.email, "setting_name": setting_name, "setting": setting_value, } if setting_name == "default_language": assert isinstance(setting_value, str) legacy_event["language_name"] = get_language_name(setting_value) transaction.on_commit(lambda: send_event( user_profile.realm, legacy_event, [user_profile.id])) # Updates to the time zone display setting are sent to all users if setting_name == "timezone": payload = dict( email=user_profile.email, user_id=user_profile.id, timezone=canonicalize_timezone(user_profile.timezone), ) timezone_event = dict(type="realm_user", op="update", person=payload) transaction.on_commit(lambda: send_event( user_profile.realm, timezone_event, active_user_ids(user_profile.realm_id), )) if setting_name == "enable_drafts_synchronization" and setting_value is False: # Delete all of the drafts from the backend but don't send delete events # for them since all that's happened is that we stopped syncing changes, # not deleted every previously synced draft - to do that use the DELETE # endpoint. Draft.objects.filter(user_profile=user_profile).delete()