2020-06-11 00:54:34 +02:00
|
|
|
from typing import Any, Dict, List, Optional, Tuple
|
2018-11-01 15:16:26 +01:00
|
|
|
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.db import connection
|
|
|
|
from django.db.models.query import Q, QuerySet
|
|
|
|
from sqlalchemy.sql import column, func, literal
|
2018-11-01 21:48:49 +01:00
|
|
|
|
2018-11-09 18:35:34 +01:00
|
|
|
from zerver.lib.request import REQ
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.models import Message, Recipient, Stream, UserMessage, UserProfile
|
2018-11-01 15:16:26 +01:00
|
|
|
|
2018-11-01 18:26:20 +01:00
|
|
|
# Only use these constants for events.
|
|
|
|
ORIG_TOPIC = "orig_subject"
|
|
|
|
TOPIC_NAME = "subject"
|
2020-02-07 13:09:17 +01:00
|
|
|
TOPIC_LINKS = "topic_links"
|
2018-11-09 17:25:57 +01:00
|
|
|
MATCH_TOPIC = "match_subject"
|
2018-11-01 18:26:20 +01:00
|
|
|
|
2018-11-09 17:53:59 +01:00
|
|
|
# This constant is actually embedded into
|
|
|
|
# the JSON data for message edit history,
|
|
|
|
# so we'll always need to handle legacy data
|
|
|
|
# unless we do a pretty tricky migration.
|
|
|
|
LEGACY_PREV_TOPIC = "prev_subject"
|
|
|
|
|
2018-11-10 17:10:45 +01:00
|
|
|
# This constant is pretty closely coupled to the
|
|
|
|
# database, but it's the JSON field.
|
|
|
|
EXPORT_TOPIC_NAME = "subject"
|
|
|
|
|
2018-11-10 22:50:28 +01:00
|
|
|
'''
|
|
|
|
The following functions are for user-facing APIs
|
|
|
|
where we'll want to support "subject" for a while.
|
|
|
|
'''
|
|
|
|
|
|
|
|
def get_topic_from_message_info(message_info: Dict[str, Any]) -> str:
|
|
|
|
'''
|
|
|
|
Use this where you are getting dicts that are based off of messages
|
|
|
|
that may come from the outside world, especially from third party
|
|
|
|
APIs and bots.
|
|
|
|
|
|
|
|
We prefer 'topic' to 'subject' here. We expect at least one field
|
|
|
|
to be present (or the caller must know how to handle KeyError).
|
|
|
|
'''
|
|
|
|
if 'topic' in message_info:
|
|
|
|
return message_info['topic']
|
|
|
|
|
|
|
|
return message_info['subject']
|
|
|
|
|
2018-11-09 18:35:34 +01:00
|
|
|
def REQ_topic() -> Optional[str]:
|
|
|
|
# REQ handlers really return a REQ, but we
|
|
|
|
# lie to make the rest of the type matching work.
|
|
|
|
return REQ(
|
2018-11-09 19:45:25 +01:00
|
|
|
whence='topic',
|
|
|
|
aliases=['subject'],
|
2018-11-09 18:35:34 +01:00
|
|
|
converter=lambda x: x.strip(),
|
|
|
|
default=None,
|
2019-08-10 00:30:34 +02:00
|
|
|
)
|
2018-11-09 18:35:34 +01:00
|
|
|
|
2018-11-10 23:01:45 +01:00
|
|
|
'''
|
|
|
|
TRY TO KEEP THIS DIVIDING LINE.
|
|
|
|
|
|
|
|
Below this line we want to make it so that functions are only
|
|
|
|
using "subject" in the DB sense, and nothing customer facing.
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
# This is used in low-level message functions in
|
|
|
|
# zerver/lib/message.py, and it's not user facing.
|
|
|
|
DB_TOPIC_NAME = "subject"
|
|
|
|
MESSAGE__TOPIC = 'message__subject'
|
|
|
|
|
2018-11-01 21:48:49 +01:00
|
|
|
def topic_match_sa(topic_name: str) -> Any:
|
|
|
|
# _sa is short for Sql Alchemy, which we use mostly for
|
|
|
|
# queries that search messages
|
2018-11-01 22:15:43 +01:00
|
|
|
topic_cond = func.upper(column("subject")) == func.upper(literal(topic_name))
|
2018-11-01 21:48:49 +01:00
|
|
|
return topic_cond
|
|
|
|
|
2018-11-09 17:06:00 +01:00
|
|
|
def topic_column_sa() -> Any:
|
|
|
|
return column("subject")
|
|
|
|
|
2018-11-01 15:48:14 +01:00
|
|
|
def filter_by_exact_message_topic(query: QuerySet, message: Message) -> QuerySet:
|
|
|
|
topic_name = message.topic_name()
|
|
|
|
return query.filter(subject=topic_name)
|
|
|
|
|
2018-11-01 18:06:55 +01:00
|
|
|
def filter_by_topic_name_via_message(query: QuerySet, topic_name: str) -> QuerySet:
|
|
|
|
return query.filter(message__subject__iexact=topic_name)
|
|
|
|
|
2020-02-11 16:04:05 +01:00
|
|
|
def messages_for_topic(stream_recipient_id: int, topic_name: str) -> QuerySet:
|
2018-11-09 19:02:54 +01:00
|
|
|
return Message.objects.filter(
|
2020-02-11 16:04:05 +01:00
|
|
|
recipient_id=stream_recipient_id,
|
2019-01-25 02:45:55 +01:00
|
|
|
subject__iexact=topic_name,
|
2018-11-09 19:02:54 +01:00
|
|
|
)
|
|
|
|
|
2018-11-01 20:12:59 +01:00
|
|
|
def save_message_for_edit_use_case(message: Message) -> None:
|
2019-09-24 21:10:56 +02:00
|
|
|
message.save(update_fields=[TOPIC_NAME, "content", "rendered_content",
|
2018-11-01 20:12:59 +01:00
|
|
|
"rendered_content_version", "last_edit_time",
|
2019-09-24 21:10:56 +02:00
|
|
|
"edit_history", "has_attachment", "has_image",
|
2020-02-27 01:12:07 +01:00
|
|
|
"has_link", "recipient_id"])
|
2019-09-24 21:10:56 +02:00
|
|
|
|
2018-11-01 20:12:59 +01:00
|
|
|
|
2018-11-09 17:32:08 +01:00
|
|
|
def user_message_exists_for_topic(user_profile: UserProfile,
|
|
|
|
recipient: Recipient,
|
|
|
|
topic_name: str) -> bool:
|
|
|
|
return UserMessage.objects.filter(
|
|
|
|
user_profile=user_profile,
|
|
|
|
message__recipient=recipient,
|
|
|
|
message__subject__iexact=topic_name,
|
|
|
|
).exists()
|
|
|
|
|
2018-11-01 19:55:14 +01:00
|
|
|
def update_messages_for_topic_edit(message: Message,
|
|
|
|
propagate_mode: str,
|
|
|
|
orig_topic_name: str,
|
2020-02-19 01:38:34 +01:00
|
|
|
topic_name: Optional[str],
|
|
|
|
new_stream: Optional[Stream]) -> List[Message]:
|
2020-06-13 12:39:11 +02:00
|
|
|
propagate_query = Q(recipient = message.recipient, subject__iexact = orig_topic_name)
|
2018-11-01 19:55:14 +01:00
|
|
|
if propagate_mode == 'change_all':
|
2020-04-15 20:57:21 +02:00
|
|
|
propagate_query = propagate_query & ~Q(id = message.id)
|
2018-11-01 19:55:14 +01:00
|
|
|
if propagate_mode == 'change_later':
|
|
|
|
propagate_query = propagate_query & Q(id__gt = message.id)
|
|
|
|
|
|
|
|
messages = Message.objects.filter(propagate_query).select_related()
|
|
|
|
|
2020-07-02 03:51:22 +02:00
|
|
|
update_fields: Dict[str, object] = {}
|
2020-02-19 01:38:34 +01:00
|
|
|
|
2018-11-01 19:55:14 +01:00
|
|
|
# Evaluate the query before running the update
|
|
|
|
messages_list = list(messages)
|
|
|
|
|
2020-02-19 01:38:34 +01:00
|
|
|
# The cached ORM objects are not changed by the upcoming
|
|
|
|
# messages.update(), and the remote cache update (done by the
|
|
|
|
# caller) requires the new value, so we manually update the
|
|
|
|
# objects in addition to sending a bulk query to the database.
|
|
|
|
if new_stream is not None:
|
|
|
|
update_fields["recipient"] = new_stream.recipient
|
|
|
|
for m in messages_list:
|
|
|
|
m.recipient = new_stream.recipient
|
|
|
|
if topic_name is not None:
|
|
|
|
update_fields["subject"] = topic_name
|
|
|
|
for m in messages_list:
|
|
|
|
m.set_topic_name(topic_name)
|
|
|
|
|
|
|
|
messages.update(**update_fields)
|
2018-11-01 19:55:14 +01:00
|
|
|
|
|
|
|
return messages_list
|
|
|
|
|
2018-11-01 15:16:26 +01:00
|
|
|
def generate_topic_history_from_db_rows(rows: List[Tuple[str, int]]) -> List[Dict[str, Any]]:
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
canonical_topic_names: Dict[str, Tuple[int, str]] = {}
|
2018-11-01 15:16:26 +01:00
|
|
|
|
|
|
|
# Sort rows by max_message_id so that if a topic
|
|
|
|
# has many different casings, we use the most
|
|
|
|
# recent row.
|
|
|
|
rows = sorted(rows, key=lambda tup: tup[1])
|
|
|
|
|
|
|
|
for (topic_name, max_message_id) in rows:
|
|
|
|
canonical_name = topic_name.lower()
|
|
|
|
canonical_topic_names[canonical_name] = (max_message_id, topic_name)
|
|
|
|
|
|
|
|
history = []
|
|
|
|
for canonical_topic, (max_message_id, topic_name) in canonical_topic_names.items():
|
|
|
|
history.append(dict(
|
|
|
|
name=topic_name,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
max_id=max_message_id),
|
2018-11-01 15:16:26 +01:00
|
|
|
)
|
|
|
|
return sorted(history, key=lambda x: -x['max_id'])
|
|
|
|
|
2020-08-26 02:03:48 +02:00
|
|
|
def get_topic_history_for_public_stream(recipient: Recipient) -> List[Dict[str, Any]]:
|
2020-08-26 02:03:08 +02:00
|
|
|
cursor = connection.cursor()
|
|
|
|
query = '''
|
|
|
|
SELECT
|
|
|
|
"zerver_message"."subject" as topic,
|
|
|
|
max("zerver_message".id) as max_message_id
|
|
|
|
FROM "zerver_message"
|
|
|
|
WHERE (
|
|
|
|
"zerver_message"."recipient_id" = %s
|
|
|
|
)
|
|
|
|
GROUP BY (
|
|
|
|
"zerver_message"."subject"
|
|
|
|
)
|
|
|
|
ORDER BY max("zerver_message".id) DESC
|
|
|
|
'''
|
2020-08-26 02:03:48 +02:00
|
|
|
cursor.execute(query, [recipient.id])
|
2018-11-01 15:16:26 +01:00
|
|
|
rows = cursor.fetchall()
|
|
|
|
cursor.close()
|
|
|
|
|
|
|
|
return generate_topic_history_from_db_rows(rows)
|
|
|
|
|
2020-08-26 02:03:48 +02:00
|
|
|
def get_topic_history_for_stream(user_profile: UserProfile,
|
|
|
|
recipient: Recipient,
|
|
|
|
public_history: bool) -> List[Dict[str, Any]]:
|
|
|
|
if public_history:
|
|
|
|
return get_topic_history_for_public_stream(recipient)
|
|
|
|
|
2018-11-01 15:16:26 +01:00
|
|
|
cursor = connection.cursor()
|
|
|
|
query = '''
|
|
|
|
SELECT
|
|
|
|
"zerver_message"."subject" as topic,
|
|
|
|
max("zerver_message".id) as max_message_id
|
|
|
|
FROM "zerver_message"
|
2020-08-26 02:03:48 +02:00
|
|
|
INNER JOIN "zerver_usermessage" ON (
|
|
|
|
"zerver_usermessage"."message_id" = "zerver_message"."id"
|
|
|
|
)
|
2018-11-01 15:16:26 +01:00
|
|
|
WHERE (
|
2020-08-26 02:03:48 +02:00
|
|
|
"zerver_usermessage"."user_profile_id" = %s AND
|
2018-11-01 15:16:26 +01:00
|
|
|
"zerver_message"."recipient_id" = %s
|
|
|
|
)
|
|
|
|
GROUP BY (
|
|
|
|
"zerver_message"."subject"
|
|
|
|
)
|
|
|
|
ORDER BY max("zerver_message".id) DESC
|
|
|
|
'''
|
2020-08-26 02:03:48 +02:00
|
|
|
cursor.execute(query, [user_profile.id, recipient.id])
|
2018-11-01 15:16:26 +01:00
|
|
|
rows = cursor.fetchall()
|
|
|
|
cursor.close()
|
|
|
|
|
|
|
|
return generate_topic_history_from_db_rows(rows)
|