pointer: Remove pointer from API and page_params.

There is still some miscellaneous cleanup that
has to happen for things like analytics queries
and dead code in node tests, but this should
remove the main use of pointers in the backend.

(We will also still need to drop the DB field.)
This commit is contained in:
Steve Howell 2020-06-26 17:51:10 +00:00 committed by Tim Abbott
parent 256982b3f8
commit 69be97e365
15 changed files with 23 additions and 250 deletions

View File

@ -10,6 +10,12 @@ below features are supported.
## Changes in Zulip 3.0 ## Changes in Zulip 3.0
**Feature level 23**
* `GET/PUT/POST /users/me/pointer`: Eliminated. We eliminated
the whole concept of the "global" user pointer leading up to
this commit.
**Feature level 22** **Feature level 22**
* `GET /attachments`: The date when a message using the attachment was * `GET /attachments`: The date when a message using the attachment was

View File

@ -29,7 +29,7 @@ DESKTOP_WARNING_VERSION = "5.2.0"
# #
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# new level means in templates/zerver/api/changelog.md. # new level means in templates/zerver/api/changelog.md.
API_FEATURE_LEVEL = 22 API_FEATURE_LEVEL = 23
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump # only when going from an old version of the code to a newer version. Bump

View File

@ -4031,42 +4031,6 @@ def update_user_presence(user_profile: UserProfile, client: Client, log_time: da
if new_user_input: if new_user_input:
update_user_activity_interval(user_profile, log_time) update_user_activity_interval(user_profile, log_time)
def do_update_pointer(user_profile: UserProfile, client: Client,
pointer: int, update_flags: bool=False) -> None:
prev_pointer = user_profile.pointer
user_profile.pointer = pointer
user_profile.save(update_fields=["pointer"])
if update_flags: # nocoverage
# This block of code is compatibility code for the
# legacy/original Zulip Android app natively. It's a shim
# that will mark as read any messages up until the pointer
# move; we expect to remove this feature entirely before long,
# when we drop support for the old Android app entirely.
app_message_ids = UserMessage.objects.filter(
user_profile=user_profile,
message__id__gt=prev_pointer,
message__id__lte=pointer).extra(where=[
UserMessage.where_unread(),
UserMessage.where_active_push_notification(),
]).values_list("message_id", flat=True)
UserMessage.objects.filter(user_profile=user_profile,
message__id__gt=prev_pointer,
message__id__lte=pointer).extra(where=[UserMessage.where_unread()]) \
.update(flags=F('flags').bitor(UserMessage.flags.read))
do_clear_mobile_push_notifications_for_ids([user_profile.id], app_message_ids)
event_time = timezone_now()
count = len(app_message_ids)
do_increment_logging_stat(user_profile, COUNT_STATS['messages_read::hour'],
None, event_time, increment=count)
do_increment_logging_stat(user_profile, COUNT_STATS['messages_read_interactions::hour'],
None, event_time, increment=min(1, count))
event = dict(type='pointer', pointer=pointer)
send_event(user_profile.realm, event, [user_profile.id])
def do_update_user_status(user_profile: UserProfile, def do_update_user_status(user_profile: UserProfile,
away: Optional[bool], away: Optional[bool],
status_text: Optional[str], status_text: Optional[str],

View File

@ -131,9 +131,6 @@ def fetch_initial_state_data(user_profile: UserProfile,
if want('muted_topics'): if want('muted_topics'):
state['muted_topics'] = get_topic_mutes(user_profile) state['muted_topics'] = get_topic_mutes(user_profile)
if want('pointer'):
state['pointer'] = user_profile.pointer
if want('presence'): if want('presence'):
state['presences'] = get_presences_for_realm(realm, slim_presence) state['presences'] = get_presences_for_realm(realm, slim_presence)
@ -398,8 +395,6 @@ def apply_event(state: Dict[str, Any],
state['hotspots'] = event['hotspots'] state['hotspots'] = event['hotspots']
elif event['type'] == "custom_profile_fields": elif event['type'] == "custom_profile_fields":
state['custom_profile_fields'] = event['fields'] state['custom_profile_fields'] = event['fields']
elif event['type'] == "pointer":
state['pointer'] = max(state['pointer'], event['pointer'])
elif event['type'] == "realm_user": elif event['type'] == "realm_user":
person = event['person'] person = event['person']
person_user_id = person['user_id'] person_user_id = person['user_id']

View File

@ -20,7 +20,7 @@ EXCLUDE_PROPERTIES = {
}, },
'/register': { '/register': {
'post': { 'post': {
'200': ['max_message_id', 'realm_emoji', 'pointer'], '200': ['max_message_id', 'realm_emoji'],
}, },
}, },
'/settings/notifications': { '/settings/notifications': {

View File

@ -86,7 +86,6 @@ from zerver.lib.actions import (
do_update_message, do_update_message,
do_update_message_flags, do_update_message_flags,
do_update_outgoing_webhook_service, do_update_outgoing_webhook_service,
do_update_pointer,
do_update_user_custom_profile_data_if_changed, do_update_user_custom_profile_data_if_changed,
do_update_user_group_description, do_update_user_group_description,
do_update_user_group_name, do_update_user_group_name,
@ -293,9 +292,7 @@ class EventsEndpointTest(ZulipTestCase):
# Check that we didn't fetch the messages data # Check that we didn't fetch the messages data
self.assertNotIn('max_message_id', result_dict) self.assertNotIn('max_message_id', result_dict)
# Check that the realm_emoji data is in there, and is correctly # Check that the realm_emoji data is in there.
# updated (presering our atomicity guaranteed), though of
# course any future pointer events won't be distributed
self.assertIn('realm_emoji', result_dict) self.assertIn('realm_emoji', result_dict)
self.assertEqual(result_dict['realm_emoji'], []) self.assertEqual(result_dict['realm_emoji'], [])
self.assertEqual(result_dict['queue_id'], '15:13') self.assertEqual(result_dict['queue_id'], '15:13')
@ -1252,14 +1249,6 @@ class EventsRegisterTest(ZulipTestCase):
self.user_profile, get_client("ZulipAndroid/1.0"), timezone_now(), UserPresence.IDLE)) self.user_profile, get_client("ZulipAndroid/1.0"), timezone_now(), UserPresence.IDLE))
schema_checker_android('events[0]', events[0]) schema_checker_android('events[0]', events[0])
def test_pointer_events(self) -> None:
schema_checker = self.check_events_dict([
('type', equals('pointer')),
('pointer', check_int),
])
events = self.do_test(lambda: do_update_pointer(self.user_profile, get_client("website"), 1500))
schema_checker('events[0]', events[0])
def test_register_events(self) -> None: def test_register_events(self) -> None:
realm_user_add_checker = self.check_events_dict([ realm_user_add_checker = self.check_events_dict([
('type', equals('realm_user')), ('type', equals('realm_user')),
@ -3801,7 +3790,6 @@ class FetchQueriesTest(ZulipTestCase):
hotspots=0, hotspots=0,
message=1, message=1,
muted_topics=1, muted_topics=1,
pointer=0,
presence=1, presence=1,
realm=0, realm=0,
realm_bot=1, realm_bot=1,

View File

@ -122,7 +122,6 @@ class HomeTest(ZulipTestCase):
"password_min_guesses", "password_min_guesses",
"password_min_length", "password_min_length",
"pm_content_in_desktop_notifications", "pm_content_in_desktop_notifications",
"pointer",
"poll_timeout", "poll_timeout",
"presence_enabled", "presence_enabled",
"presences", "presences",
@ -456,15 +455,6 @@ class HomeTest(ZulipTestCase):
self.assertEqual(mock.call_args_list[0][0][0], "Invalid narrow requested, ignoring") self.assertEqual(mock.call_args_list[0][0][0], "Invalid narrow requested, ignoring")
self._sanity_check(result) self._sanity_check(result)
def test_bad_pointer(self) -> None:
user_profile = self.example_user('hamlet')
user_profile.pointer = 999999
user_profile.save()
self.login_user(user_profile)
result = self._get_home_page()
self._sanity_check(result)
def test_topic_narrow(self) -> None: def test_topic_narrow(self) -> None:
self.login('hamlet') self.login('hamlet')
result = self._get_home_page(stream='Denmark', topic='lunch') result = self._get_home_page(stream='Denmark', topic='lunch')
@ -658,7 +648,6 @@ class HomeTest(ZulipTestCase):
page_params = self._get_page_params(result) page_params = self._get_page_params(result)
self.assertEqual(page_params['narrow_stream'], stream_name) self.assertEqual(page_params['narrow_stream'], stream_name)
self.assertEqual(page_params['narrow'], [dict(operator='stream', operand=stream_name)]) self.assertEqual(page_params['narrow'], [dict(operator='stream', operand=stream_name)])
self.assertEqual(page_params['pointer'], -1)
self.assertEqual(page_params['max_message_id'], -1) self.assertEqual(page_params['max_message_id'], -1)
def test_invites_by_admins_only(self) -> None: def test_invites_by_admins_only(self) -> None:

View File

@ -283,8 +283,6 @@ class OpenAPIArgumentsTest(ZulipTestCase):
'/settings/display', '/settings/display',
# Much more valuable would be an org admin bulk-upload feature. # Much more valuable would be an org admin bulk-upload feature.
'/users/me/profile_data', '/users/me/profile_data',
# To be deprecated and deleted.
'/users/me/pointer',
#### Should be documented as part of interactive bots documentation #### Should be documented as part of interactive bots documentation
'/bot_storage', '/bot_storage',

View File

@ -8,77 +8,19 @@ from zerver.lib.fix_unreads import fix, fix_unsubscribed
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import get_subscription, tornado_redirected_to_list from zerver.lib.test_helpers import get_subscription, tornado_redirected_to_list
from zerver.lib.topic_mutes import add_topic_mute from zerver.lib.topic_mutes import add_topic_mute
from zerver.models import Subscription, UserMessage, UserProfile, get_realm, get_stream, get_user from zerver.models import Subscription, UserMessage, UserProfile, get_realm, get_stream
class PointerTest(ZulipTestCase): class FirstUnreadAnchorTests(ZulipTestCase):
'''
HISTORICAL NOTE:
def test_update_pointer(self) -> None: The two tests in this class were originally written when
""" we had the concept of a "pointer", and they may be a bit
Posting a pointer to /update (in the form {"pointer": pointer}) changes redundant in what they now check.
the pointer we store for your UserProfile. '''
""" def test_use_first_unread_anchor(self) -> None:
self.login('hamlet') self.login('hamlet')
self.assertEqual(self.example_user('hamlet').pointer, -1)
msg_id = self.send_stream_message(self.example_user("othello"), "Verona")
result = self.client_post("/json/users/me/pointer", {"pointer": msg_id})
self.assert_json_success(result)
self.assertEqual(self.example_user('hamlet').pointer, msg_id)
def test_api_update_pointer(self) -> None:
"""
Same as above, but for the API view
"""
user = self.example_user('hamlet')
email = user.email
self.assertEqual(user.pointer, -1)
msg_id = self.send_stream_message(self.example_user("othello"), "Verona")
result = self.api_post(user, "/api/v1/users/me/pointer", {"pointer": msg_id})
self.assert_json_success(result)
self.assertEqual(get_user(email, user.realm).pointer, msg_id)
def test_missing_pointer(self) -> None:
"""
Posting json to /json/users/me/pointer which does not contain a pointer key/value pair
returns a 400 and error message.
"""
self.login('hamlet')
self.assertEqual(self.example_user('hamlet').pointer, -1)
result = self.client_post("/json/users/me/pointer", {"foo": 1})
self.assert_json_error(result, "Missing 'pointer' argument")
self.assertEqual(self.example_user('hamlet').pointer, -1)
def test_invalid_pointer(self) -> None:
"""
Posting json to /json/users/me/pointer with an invalid pointer returns a 400 and error
message.
"""
self.login('hamlet')
self.assertEqual(self.example_user('hamlet').pointer, -1)
result = self.client_post("/json/users/me/pointer", {"pointer": "foo"})
self.assert_json_error(result, "Bad value for 'pointer': foo")
self.assertEqual(self.example_user('hamlet').pointer, -1)
def test_pointer_out_of_range(self) -> None:
"""
Posting json to /json/users/me/pointer with an out of range (< 0) pointer returns a 400
and error message.
"""
self.login('hamlet')
self.assertEqual(self.example_user('hamlet').pointer, -1)
result = self.client_post("/json/users/me/pointer", {"pointer": -2})
self.assert_json_error(result, "Bad value for 'pointer': -2")
self.assertEqual(self.example_user('hamlet').pointer, -1)
def test_use_first_unread_anchor_interaction_with_pointer(self) -> None:
"""
Getting old messages (a get request to /json/messages) should never
return an unread message older than the current pointer, when there's
no narrow set.
"""
self.login('hamlet')
# Ensure the pointer is not set (-1)
self.assertEqual(self.example_user('hamlet').pointer, -1)
# Mark all existing messages as read # Mark all existing messages as read
result = self.client_post("/json/mark_all_as_read") result = self.client_post("/json/mark_all_as_read")
@ -107,7 +49,6 @@ class PointerTest(ZulipTestCase):
messages = self.get_messages( messages = self.get_messages(
anchor=0, num_before=0, num_after=2, use_first_unread_anchor=False) anchor=0, num_before=0, num_after=2, use_first_unread_anchor=False)
old_message_id = messages[0]['id'] old_message_id = messages[0]['id']
next_old_message_id = messages[1]['id']
# Verify the message is marked as read # Verify the message is marked as read
user_message = UserMessage.objects.get( user_message = UserMessage.objects.get(
@ -135,23 +76,8 @@ class PointerTest(ZulipTestCase):
self.assertEqual(messages_response['messages'][0]['id'], old_message_id) self.assertEqual(messages_response['messages'][0]['id'], old_message_id)
self.assertEqual(messages_response['anchor'], old_message_id) self.assertEqual(messages_response['anchor'], old_message_id)
# Let's update the pointer to be *after* this old unread message (but
# still on or before the new unread message we just sent)
result = self.client_post("/json/users/me/pointer",
{"pointer": next_old_message_id})
self.assert_json_success(result)
self.assertEqual(self.example_user('hamlet').pointer,
next_old_message_id)
# Verify that moving the pointer didn't mark our message as read.
user_message = UserMessage.objects.get(
message_id=old_message_id,
user_profile=self.example_user('hamlet'))
self.assertFalse(user_message.flags.read)
def test_visible_messages_use_first_unread_anchor(self) -> None: def test_visible_messages_use_first_unread_anchor(self) -> None:
self.login('hamlet') self.login('hamlet')
self.assertEqual(self.example_user('hamlet').pointer, -1)
result = self.client_post("/json/mark_all_as_read") result = self.client_post("/json/mark_all_as_read")
self.assert_json_success(result) self.assert_json_success(result)
@ -184,6 +110,7 @@ class PointerTest(ZulipTestCase):
anchor="first_unread", num_before=0, num_after=1) anchor="first_unread", num_before=0, num_after=1)
self.assert_length(messages, 1) self.assert_length(messages, 1)
class UnreadCountTests(ZulipTestCase): class UnreadCountTests(ZulipTestCase):
def setUp(self) -> None: def setUp(self) -> None:
super().setUp() super().setUp()

View File

@ -64,7 +64,6 @@ class PublicURLTest(ZulipTestCase):
"/json/subscriptions/exists", "/json/subscriptions/exists",
"/api/v1/users/me/subscriptions/properties", "/api/v1/users/me/subscriptions/properties",
"/json/fetch_api_key", "/json/fetch_api_key",
"/json/users/me/pointer",
"/json/users/me/subscriptions", "/json/users/me/subscriptions",
"/api/v1/users/me/subscriptions", "/api/v1/users/me/subscriptions",
"/json/export/realm", "/json/export/realm",
@ -76,16 +75,13 @@ class PublicURLTest(ZulipTestCase):
patch_urls = { patch_urls = {
401: ["/json/settings"], 401: ["/json/settings"],
} }
put_urls = {401: ["/json/users/me/pointer"],
}
for status_code, url_set in get_urls.items(): for status_code, url_set in get_urls.items():
self.fetch("client_get", url_set, status_code) self.fetch("client_get", url_set, status_code)
for status_code, url_set in post_urls.items(): for status_code, url_set in post_urls.items():
self.fetch("client_post", url_set, status_code) self.fetch("client_post", url_set, status_code)
for status_code, url_set in patch_urls.items(): for status_code, url_set in patch_urls.items():
self.fetch("client_patch", url_set, status_code) self.fetch("client_patch", url_set, status_code)
for status_code, url_set in put_urls.items():
self.fetch("client_put", url_set, status_code)
def test_get_gcid_when_not_configured(self) -> None: def test_get_gcid_when_not_configured(self) -> None:
with self.settings(GOOGLE_CLIENT_ID=None): with self.settings(GOOGLE_CLIENT_ID=None):

View File

@ -1531,25 +1531,6 @@ class BulkUsersTest(ZulipTestCase):
class GetProfileTest(ZulipTestCase): class GetProfileTest(ZulipTestCase):
def common_update_pointer(self, user: UserProfile, pointer: int) -> None:
self.login_user(user)
result = self.client_post("/json/users/me/pointer", {"pointer": pointer})
self.assert_json_success(result)
def common_get_pointer(self, user_id: str) -> Dict[str, Any]:
user_profile = self.example_user(user_id)
result = self.api_get(user_profile, "/json/users/me/pointer")
self.assert_json_success(result)
json = result.json()
return json
def test_get_pointer(self) -> None:
user = self.example_user("hamlet")
self.login_user(user)
result = self.client_get("/json/users/me/pointer")
self.assert_json_success(result)
self.assertIn("pointer", result.json())
def test_cache_behavior(self) -> None: def test_cache_behavior(self) -> None:
"""Tests whether fetching a user object the normal way, with """Tests whether fetching a user object the normal way, with
`get_user`, makes 1 cache query and 1 database query. `get_user`, makes 1 cache query and 1 database query.
@ -1617,33 +1598,6 @@ class GetProfileTest(ZulipTestCase):
self.assertEqual(result['user']['email'], bot.email) self.assertEqual(result['user']['email'], bot.email)
self.assertTrue(result['user']['is_bot']) self.assertTrue(result['user']['is_bot'])
def test_api_get_empty_profile(self) -> None:
"""
Ensure GET /users/me returns a max message id and returns successfully
"""
json = self.common_get_pointer("othello")
self.assertEqual(json['pointer'], -1)
def test_profile_with_pointer(self) -> None:
"""
Ensure GET /users/me returns a proper pointer id after the pointer is updated
"""
id1 = self.send_stream_message(self.example_user("othello"), "Verona")
id2 = self.send_stream_message(self.example_user("othello"), "Verona")
hamlet = self.example_user('hamlet')
self.common_update_pointer(hamlet, id2)
json = self.common_get_pointer("hamlet")
self.assertEqual(json["pointer"], id2)
self.common_update_pointer(hamlet, id1)
json = self.common_get_pointer("hamlet")
self.assertEqual(json["pointer"], id2) # pointer does not move backwards
result = self.client_post("/json/users/me/pointer", {"pointer": 99999999})
self.assert_json_error(result, "Invalid message ID")
def test_get_all_profiles_avatar_urls(self) -> None: def test_get_all_profiles_avatar_urls(self) -> None:
hamlet = self.example_user('hamlet') hamlet = self.example_user('hamlet')
result = self.api_get(hamlet, "/api/v1/users") result = self.api_get(hamlet, "/api/v1/users")

View File

@ -303,7 +303,7 @@ class EventQueue:
event['id'] = self.next_event_id event['id'] = self.next_event_id
self.next_event_id += 1 self.next_event_id += 1
full_event_type = compute_full_event_type(event) full_event_type = compute_full_event_type(event)
if (full_event_type in ["pointer", "restart"] or if (full_event_type == "restart" or
full_event_type.startswith("flags/")): full_event_type.startswith("flags/")):
if full_event_type not in self.virtual_events: if full_event_type not in self.virtual_events:
self.virtual_events[full_event_type] = copy.deepcopy(event) self.virtual_events[full_event_type] = copy.deepcopy(event)
@ -313,9 +313,8 @@ class EventQueue:
virtual_event["id"] = event["id"] virtual_event["id"] = event["id"]
if "timestamp" in event: if "timestamp" in event:
virtual_event["timestamp"] = event["timestamp"] virtual_event["timestamp"] = event["timestamp"]
if full_event_type == "pointer":
virtual_event["pointer"] = event["pointer"] if full_event_type == "restart":
elif full_event_type == "restart":
virtual_event["server_generation"] = event["server_generation"] virtual_event["server_generation"] = event["server_generation"]
elif full_event_type.startswith("flags/"): elif full_event_type.startswith("flags/"):
virtual_event["messages"] += event["messages"] virtual_event["messages"] += event["messages"]

View File

@ -212,15 +212,6 @@ def home_real(request: HttpRequest) -> HttpResponse:
) )
needs_tutorial = user_profile.tutorial_status == UserProfile.TUTORIAL_WAITING needs_tutorial = user_profile.tutorial_status == UserProfile.TUTORIAL_WAITING
if user_profile.pointer == -1:
# Put the new user's pointer at the bottom
#
# This improves performance, because we limit backfilling of messages
# before the pointer. It's also likely that someone joining an
# organization is interested in recent messages more than the very
# first messages on the system.
register_ret['pointer'] = register_ret['max_message_id']
else: # nocoverage else: # nocoverage
first_in_realm = False first_in_realm = False
prompt_for_invites = False prompt_for_invites = False
@ -291,7 +282,6 @@ def home_real(request: HttpRequest) -> HttpResponse:
page_params["narrow_topic"] = narrow_topic page_params["narrow_topic"] = narrow_topic
page_params["narrow"] = [dict(operator=term[0], operand=term[1]) for term in narrow] page_params["narrow"] = [dict(operator=term[0], operand=term[1]) for term in narrow]
page_params["max_message_id"] = initial_pointer page_params["max_message_id"] = initial_pointer
page_params["pointer"] = initial_pointer
page_params["enable_desktop_notifications"] = False page_params["enable_desktop_notifications"] = False
statsd.incr('views.home') statsd.incr('views.home')

View File

@ -1,27 +0,0 @@
from django.http import HttpRequest, HttpResponse
from django.utils.translation import ugettext as _
from zerver.lib.actions import do_update_pointer
from zerver.lib.request import REQ, JsonableError, has_request_variables
from zerver.lib.response import json_success
from zerver.lib.validator import to_non_negative_int
from zerver.models import UserProfile, get_usermessage_by_message_id
def get_pointer_backend(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
return json_success({'pointer': user_profile.pointer})
@has_request_variables
def update_pointer_backend(request: HttpRequest, user_profile: UserProfile,
pointer: int=REQ(converter=to_non_negative_int)) -> HttpResponse:
if pointer <= user_profile.pointer:
return json_success()
if get_usermessage_by_message_id(user_profile, pointer) is None:
raise JsonableError(_("Invalid message ID"))
request._log_data["extra"] = f"[{pointer}]"
update_flags = (request.client.name.lower() in ['android', "zulipandroid"])
do_update_pointer(user_profile, request.client, pointer, update_flags=update_flags)
return json_success()

View File

@ -260,12 +260,6 @@ v1_api_and_json_patterns = [
path('users/me', rest_dispatch, path('users/me', rest_dispatch,
{'GET': 'zerver.views.users.get_profile_backend', {'GET': 'zerver.views.users.get_profile_backend',
'DELETE': 'zerver.views.users.deactivate_user_own_backend'}), 'DELETE': 'zerver.views.users.deactivate_user_own_backend'}),
# PUT is currently used by mobile apps, we intend to remove the PUT version
# as soon as possible. POST exists to correct the erroneous use of PUT.
path('users/me/pointer', rest_dispatch,
{'GET': 'zerver.views.pointer.get_pointer_backend',
'PUT': 'zerver.views.pointer.update_pointer_backend',
'POST': 'zerver.views.pointer.update_pointer_backend'}),
path('users/me/presence', rest_dispatch, path('users/me/presence', rest_dispatch,
{'POST': 'zerver.views.presence.update_active_status_backend'}), {'POST': 'zerver.views.presence.update_active_status_backend'}),
path('users/me/status', rest_dispatch, path('users/me/status', rest_dispatch,