Ejemplo n.º 1
0
 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")
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
def to_timezone_or_empty(var_name: str, s: str) -> str:
    if s in pytz.all_timezones_set:
        return canonicalize_timezone(s)
    else:
        return ""
Ejemplo n.º 5
0
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()