diff --git a/zerver/models/__init__.py b/zerver/models/__init__.py index 5aceaf36f3..fb3c6054e5 100644 --- a/zerver/models/__init__.py +++ b/zerver/models/__init__.py @@ -3,7 +3,7 @@ import hashlib import time -from datetime import datetime, timedelta, timezone +from datetime import timedelta from typing import Any, Callable, Dict, List, Optional, Tuple, TypedDict, TypeVar, Union import orjson @@ -18,7 +18,7 @@ from django.core.serializers.json import DjangoJSONEncoder from django.db import models from django.db.backends.base.base import BaseDatabaseWrapper from django.db.models import CASCADE, Exists, F, OuterRef, Q, QuerySet -from django.db.models.functions import Lower, Upper +from django.db.models.functions import Upper from django.db.models.signals import post_delete, post_save from django.db.models.sql.compiler import SQLCompiler from django.utils.timezone import now as timezone_now @@ -83,6 +83,7 @@ from zerver.models.streams import DefaultStream as DefaultStream from zerver.models.streams import DefaultStreamGroup as DefaultStreamGroup from zerver.models.streams import Stream as Stream from zerver.models.streams import Subscription as Subscription +from zerver.models.user_topics import UserTopic as UserTopic from zerver.models.users import RealmUserDefault as RealmUserDefault from zerver.models.users import UserBaseSettings as UserBaseSettings from zerver.models.users import UserProfile as UserProfile @@ -142,71 +143,6 @@ def query_for_ids( return query -class UserTopic(models.Model): - user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) - stream = models.ForeignKey(Stream, on_delete=CASCADE) - recipient = models.ForeignKey(Recipient, on_delete=CASCADE) - topic_name = models.CharField(max_length=MAX_TOPIC_NAME_LENGTH) - # The default value for last_updated is a few weeks before tracking - # of when topics were muted was first introduced. It's designed - # to be obviously incorrect so that one can tell it's backfilled data. - last_updated = models.DateTimeField(default=datetime(2020, 1, 1, 0, 0, tzinfo=timezone.utc)) - - class VisibilityPolicy(models.IntegerChoices): - # A normal muted topic. No notifications and unreads hidden. - MUTED = 1, "Muted topic" - - # This topic will behave like an unmuted topic in an unmuted stream even if it - # belongs to a muted stream. - UNMUTED = 2, "Unmuted topic in muted stream" - - # This topic will behave like `UNMUTED`, plus some additional - # display and/or notifications priority that is TBD and likely to - # be configurable; see #6027. Not yet implemented. - FOLLOWED = 3, "Followed topic" - - # Implicitly, if a UserTopic does not exist, the (user, topic) - # pair should have normal behavior for that (user, stream) pair. - - # We use this in our code to represent the condition in the comment above. - INHERIT = 0, "User's default policy for the stream." - - visibility_policy = models.SmallIntegerField( - choices=VisibilityPolicy.choices, default=VisibilityPolicy.MUTED - ) - - class Meta: - constraints = [ - models.UniqueConstraint( - "user_profile", - "stream", - Lower("topic_name"), - name="usertopic_case_insensitive_topic_uniq", - ), - ] - - indexes = [ - models.Index("stream", Upper("topic_name"), name="zerver_mutedtopic_stream_topic"), - # This index is designed to optimize queries fetching the - # set of users who have special policy for a stream, - # e.g. for the send-message code paths. - models.Index( - fields=("stream", "topic_name", "visibility_policy", "user_profile"), - name="zerver_usertopic_stream_topic_user_visibility_idx", - ), - # This index is useful for handling API requests fetching the - # muted topics for a given user or user/stream pair. - models.Index( - fields=("user_profile", "visibility_policy", "stream", "topic_name"), - name="zerver_usertopic_user_visibility_idx", - ), - ] - - @override - def __str__(self) -> str: - return f"({self.user_profile.email}, {self.stream.name}, {self.topic_name}, {self.last_updated})" - - class MutedUser(models.Model): user_profile = models.ForeignKey(UserProfile, related_name="muter", on_delete=CASCADE) muted_user = models.ForeignKey(UserProfile, related_name="muted", on_delete=CASCADE) diff --git a/zerver/models/user_topics.py b/zerver/models/user_topics.py new file mode 100644 index 0000000000..0af96bd106 --- /dev/null +++ b/zerver/models/user_topics.py @@ -0,0 +1,76 @@ +from datetime import datetime, timezone + +from django.db import models +from django.db.models import CASCADE +from django.db.models.functions import Lower, Upper +from typing_extensions import override + +from zerver.models.constants import MAX_TOPIC_NAME_LENGTH +from zerver.models.recipients import Recipient +from zerver.models.streams import Stream +from zerver.models.users import UserProfile + + +class UserTopic(models.Model): + user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) + stream = models.ForeignKey(Stream, on_delete=CASCADE) + recipient = models.ForeignKey(Recipient, on_delete=CASCADE) + topic_name = models.CharField(max_length=MAX_TOPIC_NAME_LENGTH) + # The default value for last_updated is a few weeks before tracking + # of when topics were muted was first introduced. It's designed + # to be obviously incorrect so that one can tell it's backfilled data. + last_updated = models.DateTimeField(default=datetime(2020, 1, 1, 0, 0, tzinfo=timezone.utc)) + + class VisibilityPolicy(models.IntegerChoices): + # A normal muted topic. No notifications and unreads hidden. + MUTED = 1, "Muted topic" + + # This topic will behave like an unmuted topic in an unmuted stream even if it + # belongs to a muted stream. + UNMUTED = 2, "Unmuted topic in muted stream" + + # This topic will behave like `UNMUTED`, plus some additional + # display and/or notifications priority that is TBD and likely to + # be configurable; see #6027. Not yet implemented. + FOLLOWED = 3, "Followed topic" + + # Implicitly, if a UserTopic does not exist, the (user, topic) + # pair should have normal behavior for that (user, stream) pair. + + # We use this in our code to represent the condition in the comment above. + INHERIT = 0, "User's default policy for the stream." + + visibility_policy = models.SmallIntegerField( + choices=VisibilityPolicy.choices, default=VisibilityPolicy.MUTED + ) + + class Meta: + constraints = [ + models.UniqueConstraint( + "user_profile", + "stream", + Lower("topic_name"), + name="usertopic_case_insensitive_topic_uniq", + ), + ] + + indexes = [ + models.Index("stream", Upper("topic_name"), name="zerver_mutedtopic_stream_topic"), + # This index is designed to optimize queries fetching the + # set of users who have special policy for a stream, + # e.g. for the send-message code paths. + models.Index( + fields=("stream", "topic_name", "visibility_policy", "user_profile"), + name="zerver_usertopic_stream_topic_user_visibility_idx", + ), + # This index is useful for handling API requests fetching the + # muted topics for a given user or user/stream pair. + models.Index( + fields=("user_profile", "visibility_policy", "stream", "topic_name"), + name="zerver_usertopic_user_visibility_idx", + ), + ] + + @override + def __str__(self) -> str: + return f"({self.user_profile.email}, {self.stream.name}, {self.topic_name}, {self.last_updated})"