From 2f251dedafccd2fd325680ca0ccb282dbecbb433 Mon Sep 17 00:00:00 2001 From: "K.Kanakhin" Date: Thu, 2 Mar 2017 14:52:17 +0600 Subject: [PATCH] user-presence: Add aggregated status to user presence info. - Add aggregated status to user presence status dict. - Add tests for aggregated presence status. - Fix removing unused keys from status dict with aggregated data for user. Fixes #3692 --- zerver/models.py | 11 ++++-- zerver/tests/test_presence.py | 70 ++++++++++++++++++++++++++++++++++- zerver/views/presence.py | 4 +- 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/zerver/models.py b/zerver/models.py index 6dec16f2cf..7c764db1dd 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -1395,8 +1395,8 @@ class UserPresence(models.Model): def get_status_dicts_for_query(query, mobile_user_ids): # type: (QuerySet, List[int]) -> defaultdict[Any, Dict[Any, Any]] user_statuses = defaultdict(dict) # type: defaultdict[Any, Dict[Any, Any]] - - for row in query: + # Order of query is important to get a latest status as aggregated status. + for row in query.order_by("user_profile__id", "-timestamp"): info = UserPresence.to_presence_dict( row['client__name'], row['status'], @@ -1405,8 +1405,13 @@ class UserPresence(models.Model): has_push_devices=row['user_profile__id'] in mobile_user_ids, is_mirror_dummy=row['user_profile__is_mirror_dummy'], ) + if not user_statuses.get(row['user_profile__email']): + # Applying the latest status as aggregated status for user. + user_statuses[row['user_profile__email']]['aggregated'] = { + 'status': info['status'], + 'timestamp': info['timestamp'] + } user_statuses[row['user_profile__email']][row['client__name']] = info - return user_statuses @staticmethod diff --git a/zerver/tests/test_presence.py b/zerver/tests/test_presence.py index 75b0fe0761..72ad8fa32c 100644 --- a/zerver/tests/test_presence.py +++ b/zerver/tests/test_presence.py @@ -4,6 +4,7 @@ from __future__ import print_function from django.http import HttpResponse from django.utils import timezone +from mock import mock from typing import Any, Dict from zerver.lib.actions import do_deactivate_user @@ -15,6 +16,7 @@ from zerver.lib.test_helpers import ( from zerver.lib.test_classes import ( ZulipTestCase, ) +from zerver.lib.timestamp import datetime_to_timestamp from zerver.models import ( email_to_domain, Client, @@ -200,5 +202,71 @@ class SingleUserPresenceTests(ZulipTestCase): self.login("hamlet@zulip.com") result = self.client_get("/json/users/othello@zulip.com/presence") result_dict = ujson.loads(result.content) - self.assertEqual(set(result_dict['presence'].keys()), {"ZulipAndroid", "website"}) + self.assertEqual( + set(result_dict['presence'].keys()), + {"ZulipAndroid", "website", "aggregated"}) self.assertEqual(set(result_dict['presence']['website'].keys()), {"status", "timestamp"}) + + +class UserPresenceAggregationTests(ZulipTestCase): + def _send_presence_for_aggregated_tests(self, email, status, validate_time): + # type: (str, str, datetime.datetime) -> Dict[str, Dict[str, Any]] + self.login(email) + timezone_util = 'django.utils.timezone.now' + with mock.patch(timezone_util, return_value=validate_time - datetime.timedelta(seconds=5)): + self.client_post("/json/users/me/presence", {'status': status}) + with mock.patch(timezone_util, return_value=validate_time - datetime.timedelta(seconds=2)): + self.client_post("/api/v1/users/me/presence", {'status': status}, + HTTP_USER_AGENT="ZulipAndroid/1.0", + **self.api_auth(email)) + with mock.patch(timezone_util, return_value=validate_time - datetime.timedelta(seconds=7)): + self.client_post("/api/v1/users/me/presence", {'status': status}, + HTTP_USER_AGENT="ZulipIOS/1.0", + **self.api_auth(email)) + result = self.client_get("/json/users/%s/presence" % (email,)) + return result.json() + + def test_aggregated_presense_active(self): + # type: () -> None + validate_time = timezone.now() + result_dict = self._send_presence_for_aggregated_tests('othello@zulip.com', 'active', + validate_time) + self.assertDictEqual( + result_dict['presence']['aggregated'], + { + "status": "active", + "timestamp": datetime_to_timestamp(validate_time - datetime.timedelta(seconds=2)) + } + ) + + def test_aggregated_presense_idle(self): + # type: () -> None + validate_time = timezone.now() + result_dict = self._send_presence_for_aggregated_tests('othello@zulip.com', 'idle', + validate_time) + self.assertDictEqual( + result_dict['presence']['aggregated'], + { + "status": "idle", + "timestamp": datetime_to_timestamp(validate_time - datetime.timedelta(seconds=2)) + } + ) + + def test_aggregated_presense_mixed(self): + # type: () -> None + email = "othello@zulip.com" + self.login(email) + validate_time = timezone.now() + with mock.patch('django.utils.timezone.now', + return_value=validate_time - datetime.timedelta(seconds=3)): + self.client_post("/api/v1/users/me/presence", {'status': 'active'}, + HTTP_USER_AGENT="ZulipTestDev/1.0", + **self.api_auth(email)) + result_dict = self._send_presence_for_aggregated_tests(email, 'idle', validate_time) + self.assertDictEqual( + result_dict['presence']['aggregated'], + { + "status": "idle", + "timestamp": datetime_to_timestamp(validate_time - datetime.timedelta(seconds=2)) + } + ) diff --git a/zerver/views/presence.py b/zerver/views/presence.py index 99c22d7f01..b0d7a75456 100644 --- a/zerver/views/presence.py +++ b/zerver/views/presence.py @@ -41,8 +41,8 @@ def get_presence_backend(request, user_profile, email): # For initial version, we just include the status and timestamp keys result = dict(presence=presence_dict[target.email]) for val in result['presence'].values(): - del val['client'] - del val['pushable'] + val.pop('client', None) + val.pop('pushable', None) return json_success(result) @has_request_variables