mirror of
https://github.com/zulip/zulip.git
synced 2025-11-13 18:36:36 +00:00
Add get_topic_history_for_stream().
This commit is contained in:
@@ -40,7 +40,7 @@ from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity,
|
|||||||
from zerver.lib.alert_words import alert_words_in_realm
|
from zerver.lib.alert_words import alert_words_in_realm
|
||||||
from zerver.lib.avatar import get_avatar_url, avatar_url
|
from zerver.lib.avatar import get_avatar_url, avatar_url
|
||||||
|
|
||||||
from django.db import transaction, IntegrityError
|
from django.db import transaction, IntegrityError, connection
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@@ -167,6 +167,52 @@ def realm_user_count(realm):
|
|||||||
# type: (Realm) -> int
|
# type: (Realm) -> int
|
||||||
return UserProfile.objects.filter(realm=realm, is_active=True, is_bot=False).count()
|
return UserProfile.objects.filter(realm=realm, is_active=True, is_bot=False).count()
|
||||||
|
|
||||||
|
def get_topic_history_for_stream(user_profile, recipient):
|
||||||
|
# type: (UserProfile, Recipient) -> List[Tuple[str, int]]
|
||||||
|
|
||||||
|
# We tested the below query on some large prod datasets, and we never
|
||||||
|
# saw more than 50ms to execute it, so we think that's acceptable,
|
||||||
|
# but we will monitor it, and we may later optimize it further.
|
||||||
|
query = '''
|
||||||
|
SELECT topic, read, count(*)
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
("zerver_usermessage"."flags" & 1) as read,
|
||||||
|
"zerver_message"."subject" as topic,
|
||||||
|
"zerver_message"."id" as message_id
|
||||||
|
FROM "zerver_usermessage"
|
||||||
|
INNER JOIN "zerver_message" ON (
|
||||||
|
"zerver_usermessage"."message_id" = "zerver_message"."id"
|
||||||
|
) WHERE (
|
||||||
|
"zerver_usermessage"."user_profile_id" = %s AND
|
||||||
|
"zerver_message"."recipient_id" = %s
|
||||||
|
) ORDER BY "zerver_usermessage"."message_id" DESC
|
||||||
|
) messages_for_stream
|
||||||
|
GROUP BY topic, read
|
||||||
|
ORDER BY max(message_id) desc
|
||||||
|
'''
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute(query, [user_profile.id, recipient.id])
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
topic_names = dict() # type: Dict[str, str]
|
||||||
|
topic_counts = dict() # type: Dict[str, int]
|
||||||
|
topics = []
|
||||||
|
for row in rows:
|
||||||
|
topic_name, read, count = row
|
||||||
|
if topic_name.lower() not in topic_names:
|
||||||
|
topic_names[topic_name.lower()] = topic_name
|
||||||
|
topic_name = topic_names[topic_name.lower()]
|
||||||
|
if topic_name not in topic_counts:
|
||||||
|
topic_counts[topic_name] = 0
|
||||||
|
topics.append(topic_name)
|
||||||
|
if not read:
|
||||||
|
topic_counts[topic_name] += count
|
||||||
|
|
||||||
|
history = [(topic, topic_counts[topic]) for topic in topics]
|
||||||
|
return history
|
||||||
|
|
||||||
def send_signup_message(sender, signups_stream, user_profile,
|
def send_signup_message(sender, signups_stream, user_profile,
|
||||||
internal=False, realm=None):
|
internal=False, realm=None):
|
||||||
# type: (UserProfile, text_type, UserProfile, bool, Optional[Realm]) -> None
|
# type: (UserProfile, text_type, UserProfile, bool, Optional[Realm]) -> None
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from zerver.lib.actions import (
|
|||||||
check_message, check_send_message,
|
check_message, check_send_message,
|
||||||
do_create_user,
|
do_create_user,
|
||||||
get_client,
|
get_client,
|
||||||
|
get_recipient,
|
||||||
)
|
)
|
||||||
|
|
||||||
from zerver.lib.upload import create_attachment
|
from zerver.lib.upload import create_attachment
|
||||||
@@ -50,6 +51,95 @@ from six import text_type
|
|||||||
from six.moves import range
|
from six.moves import range
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
class TopicHistoryTest(ZulipTestCase):
|
||||||
|
def test_topics_history(self):
|
||||||
|
# type: () -> None
|
||||||
|
# verified: int(UserMessage.flags.read) == 1
|
||||||
|
email = 'iago@zulip.com'
|
||||||
|
stream_name = 'Verona'
|
||||||
|
self.login(email)
|
||||||
|
|
||||||
|
user_profile = get_user_profile_by_email(email)
|
||||||
|
stream = Stream.objects.get(name=stream_name)
|
||||||
|
recipient = get_recipient(Recipient.STREAM, stream.id)
|
||||||
|
|
||||||
|
def create_test_message(topic, read, starred=False):
|
||||||
|
# type: (str, bool, bool) -> None
|
||||||
|
|
||||||
|
hamlet = get_user_profile_by_email('hamlet@zulip.com')
|
||||||
|
message = Message.objects.create(
|
||||||
|
sender=hamlet,
|
||||||
|
recipient=recipient,
|
||||||
|
subject=topic,
|
||||||
|
content='whatever',
|
||||||
|
pub_date=timezone.now(),
|
||||||
|
sending_client=get_client('whatever'),
|
||||||
|
)
|
||||||
|
flags = 0
|
||||||
|
if read:
|
||||||
|
flags |= UserMessage.flags.read
|
||||||
|
|
||||||
|
# use this to make sure our query isn't confused
|
||||||
|
# by other flags
|
||||||
|
if starred:
|
||||||
|
flags |= UserMessage.flags.starred
|
||||||
|
|
||||||
|
UserMessage.objects.create(
|
||||||
|
user_profile=user_profile,
|
||||||
|
message=message,
|
||||||
|
flags=flags,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_test_message('topic2', read=False)
|
||||||
|
create_test_message('toPIc1', read=False, starred=True)
|
||||||
|
create_test_message('topic2', read=False)
|
||||||
|
create_test_message('topic2', read=True)
|
||||||
|
create_test_message('topic2', read=False, starred=True)
|
||||||
|
create_test_message('Topic2', read=False)
|
||||||
|
create_test_message('already_read', read=True)
|
||||||
|
|
||||||
|
endpoint = '/json/users/me/%d/topics' % (stream.id,)
|
||||||
|
result = self.client_get(endpoint, dict())
|
||||||
|
self.assert_json_success(result)
|
||||||
|
history = ujson.loads(result.content)['topics']
|
||||||
|
|
||||||
|
# We only look at the most recent three topics, because
|
||||||
|
# the prior fixture data may be unreliable.
|
||||||
|
self.assertEqual(history[:3], [
|
||||||
|
[u'already_read', 0],
|
||||||
|
[u'Topic2', 4],
|
||||||
|
[u'toPIc1', 1],
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_bad_stream_id(self):
|
||||||
|
# type: () -> None
|
||||||
|
email = 'iago@zulip.com'
|
||||||
|
self.login(email)
|
||||||
|
|
||||||
|
# non-sensible stream id
|
||||||
|
endpoint = '/json/users/me/9999999999/topics'
|
||||||
|
result = self.client_get(endpoint, dict())
|
||||||
|
self.assert_json_error(result, 'Invalid stream id')
|
||||||
|
|
||||||
|
# out of realm
|
||||||
|
bad_stream = self.make_stream(
|
||||||
|
'mit_stream',
|
||||||
|
realm=get_realm('mit.edu')
|
||||||
|
)
|
||||||
|
endpoint = '/json/users/me/%s/topics' % (bad_stream.id,)
|
||||||
|
result = self.client_get(endpoint, dict())
|
||||||
|
self.assert_json_error(result, 'Invalid stream id')
|
||||||
|
|
||||||
|
# private stream to which I am not subscribed
|
||||||
|
private_stream = self.make_stream(
|
||||||
|
'private_stream',
|
||||||
|
invite_only=True
|
||||||
|
)
|
||||||
|
endpoint = '/json/users/me/%s/topics' % (private_stream.id,)
|
||||||
|
result = self.client_get(endpoint, dict())
|
||||||
|
self.assert_json_error(result, 'Invalid stream id')
|
||||||
|
|
||||||
|
|
||||||
class TestCrossRealmPMs(ZulipTestCase):
|
class TestCrossRealmPMs(ZulipTestCase):
|
||||||
def make_realm(self, domain):
|
def make_realm(self, domain):
|
||||||
# type: (text_type) -> Realm
|
# type: (text_type) -> Realm
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ from django.http import HttpRequest, HttpResponse
|
|||||||
from zerver.lib.request import JsonableError, REQ, has_request_variables
|
from zerver.lib.request import JsonableError, REQ, has_request_variables
|
||||||
from zerver.decorator import authenticated_json_post_view, \
|
from zerver.decorator import authenticated_json_post_view, \
|
||||||
authenticated_json_view, \
|
authenticated_json_view, \
|
||||||
get_user_profile_by_email, require_realm_admin
|
get_user_profile_by_email, require_realm_admin, to_non_negative_int
|
||||||
from zerver.lib.actions import bulk_remove_subscriptions, \
|
from zerver.lib.actions import bulk_remove_subscriptions, \
|
||||||
do_change_subscription_property, internal_prep_message, \
|
do_change_subscription_property, internal_prep_message, \
|
||||||
create_streams_if_needed, gather_subscriptions, subscribed_to_stream, \
|
create_streams_if_needed, gather_subscriptions, subscribed_to_stream, \
|
||||||
bulk_add_subscriptions, do_send_messages, get_subscriber_emails, do_rename_stream, \
|
bulk_add_subscriptions, do_send_messages, get_subscriber_emails, do_rename_stream, \
|
||||||
do_deactivate_stream, do_make_stream_public, do_add_default_stream, \
|
do_deactivate_stream, do_make_stream_public, do_add_default_stream, \
|
||||||
do_change_stream_description, do_get_streams, do_make_stream_private, \
|
do_change_stream_description, do_get_streams, do_make_stream_private, \
|
||||||
do_remove_default_stream
|
do_remove_default_stream, get_topic_history_for_stream
|
||||||
from zerver.lib.response import json_success, json_error, json_response
|
from zerver.lib.response import json_success, json_error, json_response
|
||||||
from zerver.lib.validator import check_string, check_list, check_dict, \
|
from zerver.lib.validator import check_string, check_list, check_dict, \
|
||||||
check_bool, check_variable_type
|
check_bool, check_variable_type
|
||||||
@@ -453,6 +453,37 @@ def get_streams_backend(request, user_profile,
|
|||||||
include_default=include_default)
|
include_default=include_default)
|
||||||
return json_success({"streams": streams})
|
return json_success({"streams": streams})
|
||||||
|
|
||||||
|
@has_request_variables
|
||||||
|
def get_topics_backend(request, user_profile,
|
||||||
|
stream_id=REQ(converter=to_non_negative_int)):
|
||||||
|
# type: (HttpRequest, UserProfile, int) -> HttpResponse
|
||||||
|
|
||||||
|
try:
|
||||||
|
stream = Stream.objects.get(pk=stream_id)
|
||||||
|
except Stream.DoesNotExist:
|
||||||
|
return json_error(_("Invalid stream id"))
|
||||||
|
|
||||||
|
if stream.realm_id != user_profile.realm_id:
|
||||||
|
return json_error(_("Invalid stream id"))
|
||||||
|
|
||||||
|
recipient = get_recipient(Recipient.STREAM, stream.id)
|
||||||
|
|
||||||
|
if not stream.is_public():
|
||||||
|
if not is_active_subscriber(user_profile=user_profile,
|
||||||
|
recipient=recipient):
|
||||||
|
return json_error(_("Invalid stream id"))
|
||||||
|
|
||||||
|
result = get_topic_history_for_stream(
|
||||||
|
user_profile=user_profile,
|
||||||
|
recipient=recipient,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Our data structure here is a list of tuples of
|
||||||
|
# (topic name, unread count), and it's reverse chronological,
|
||||||
|
# so the most recent topic is the first element of the list.
|
||||||
|
return json_success(dict(topics=result))
|
||||||
|
|
||||||
|
|
||||||
@authenticated_json_post_view
|
@authenticated_json_post_view
|
||||||
@has_request_variables
|
@has_request_variables
|
||||||
def json_stream_exists(request, user_profile, stream=REQ(),
|
def json_stream_exists(request, user_profile, stream=REQ(),
|
||||||
|
|||||||
@@ -211,6 +211,10 @@ v1_api_and_json_patterns = [
|
|||||||
'PUT': 'zerver.views.alert_words.add_alert_words',
|
'PUT': 'zerver.views.alert_words.add_alert_words',
|
||||||
'DELETE': 'zerver.views.alert_words.remove_alert_words'}),
|
'DELETE': 'zerver.views.alert_words.remove_alert_words'}),
|
||||||
|
|
||||||
|
url(r'^users/me/(?P<stream_id>\d+)/topics$', 'zerver.lib.rest.rest_dispatch',
|
||||||
|
{'GET': 'zerver.views.streams.get_topics_backend'}),
|
||||||
|
|
||||||
|
|
||||||
# streams -> zerver.views.streams
|
# streams -> zerver.views.streams
|
||||||
# (this API is only used externally)
|
# (this API is only used externally)
|
||||||
url(r'^streams$', 'zerver.lib.rest.rest_dispatch',
|
url(r'^streams$', 'zerver.lib.rest.rest_dispatch',
|
||||||
|
|||||||
Reference in New Issue
Block a user