From 0e893b9045b56b87a4b94e715cbb9faaa303cf6b Mon Sep 17 00:00:00 2001 From: "Hemanth V. Alluri" Date: Thu, 23 Jul 2020 22:54:22 +0530 Subject: [PATCH] models/drafts: Add a model for storing Draft messages. Also add a Draft object-to-dictionary conversion method. The following commits will provide an API around this model using which our clients can sync drafts across each other (if they so wish too). As of making this commit, we haven't finalized exactly how our clients will use this. See https://chat.zulip.org/#narrow/stream/2-general/topic/drafts For some of the discussion around this model and in general, around this feature. Signed-off-by: Hemanth V. Alluri --- zerver/lib/export.py | 4 ++++ zerver/migrations/0297_draft.py | 26 +++++++++++++++++++++ zerver/models.py | 40 ++++++++++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 zerver/migrations/0297_draft.py diff --git a/zerver/lib/export.py b/zerver/lib/export.py index f237abdc53..8c0aa671b0 100644 --- a/zerver/lib/export.py +++ b/zerver/lib/export.py @@ -124,6 +124,7 @@ ALL_ZULIP_TABLES = { 'zerver_defaultstream', 'zerver_defaultstreamgroup', 'zerver_defaultstreamgroup_streams', + 'zerver_draft', 'zerver_emailchangestatus', 'zerver_huddle', 'zerver_message', @@ -237,6 +238,9 @@ NON_EXPORTED_TABLES = { # This is low priority, since users can easily just reset themselves to away. 'zerver_userstatus', + # Drafts don't need to be exported as they are supposed to be more ephemeral. + 'zerver_draft', + # For any tables listed below here, it's a bug that they are not present in the export. } diff --git a/zerver/migrations/0297_draft.py b/zerver/migrations/0297_draft.py new file mode 100644 index 0000000000..710571a0be --- /dev/null +++ b/zerver/migrations/0297_draft.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.14 on 2020-07-23 17:07 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('zerver', '0296_remove_userprofile_short_name'), + ] + + operations = [ + migrations.CreateModel( + name='Draft', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('topic', models.CharField(db_index=True, max_length=60)), + ('content', models.TextField()), + ('last_edit_time', models.DateTimeField(db_index=True)), + ('recipient', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='zerver.Recipient')), + ('user_profile', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/zerver/models.py b/zerver/models.py index 6d4c50a292..3bf4eb9a6e 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -66,7 +66,7 @@ from zerver.lib.cache import ( ) from zerver.lib.exceptions import JsonableError from zerver.lib.pysa import mark_sanitized -from zerver.lib.timestamp import datetime_to_timestamp +from zerver.lib.timestamp import datetime_to_precise_timestamp, datetime_to_timestamp from zerver.lib.types import ( DisplayRecipientT, ExtendedFieldElement, @@ -1899,6 +1899,44 @@ class ArchivedSubMessage(AbstractSubMessage): post_save.connect(flush_submessage, sender=SubMessage) +class Draft(models.Model): + """ Server-side storage model for storing drafts so that drafts can be synced across + multiple clients/devices. + """ + user_profile: UserProfile = models.ForeignKey(UserProfile, on_delete=models.CASCADE) + recipient: Optional[Recipient] = models.ForeignKey(Recipient, null=True, on_delete=models.SET_NULL) + topic: str = models.CharField(max_length=MAX_TOPIC_NAME_LENGTH, db_index=True) + content: str = models.TextField() # Length should not exceed MAX_MESSAGE_LENGTH + last_edit_time: datetime.datetime = models.DateTimeField(db_index=True) + + def __str__(self) -> str: + return f"<{self.__class__.__name__}: {self.user_profile.email} / {self.id} / {self.last_edit_time}>" + + def to_dict(self) -> Dict[str, Any]: # nocoverage # Will be added in a later commit. + if self.recipient is None: + _type = "" + to = [] + elif self.recipient.type == Recipient.STREAM: + _type = "stream" + to = [self.recipient.type_id] + else: + _type = "private" + if self.recipient.type == Recipient.PERSONAL: + to = [self.recipient.type_id] + else: + to = [] + for r in get_display_recipient(self.recipient): + assert(not isinstance(r, str)) # It will only be a string for streams + if not r["id"] == self.user_profile_id: + to.append(r["id"]) + return { + "type": _type, + "to": to, + "topic": self.topic, + "content": self.content, + "timestamp": datetime_to_precise_timestamp(self.last_edit_time), + } + class AbstractReaction(models.Model): """For emoji reactions to messages (and potentially future reaction types).