mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	drafts: Send events to clients when drafts change.
With this, the core of the new drafts system is complete.
This commit is contained in:
		
				
					committed by
					
						
						Tim Abbott
					
				
			
			
				
	
			
			
			
						parent
						
							c00089ac28
						
					
				
				
					commit
					6fee946a43
				
			@@ -24,6 +24,7 @@ from zerver.lib.validator import (
 | 
				
			|||||||
    check_union,
 | 
					    check_union,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from zerver.models import Draft, UserProfile
 | 
					from zerver.models import Draft, UserProfile
 | 
				
			||||||
 | 
					from zerver.tornado.django_api import send_event
 | 
				
			||||||
 | 
					
 | 
				
			||||||
VALID_DRAFT_TYPES: Set[str] = {"", "private", "stream"}
 | 
					VALID_DRAFT_TYPES: Set[str] = {"", "private", "stream"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -117,6 +118,14 @@ def do_create_drafts(draft_dicts: List[Dict[str, Any]], user_profile: UserProfil
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    created_draft_objects = Draft.objects.bulk_create(draft_objects)
 | 
					    created_draft_objects = Draft.objects.bulk_create(draft_objects)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    event = {
 | 
				
			||||||
 | 
					        "type": "drafts",
 | 
				
			||||||
 | 
					        "op": "add",
 | 
				
			||||||
 | 
					        "drafts": [draft.to_dict() for draft in created_draft_objects],
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    send_event(user_profile.realm, event, [user_profile.id])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return created_draft_objects
 | 
					    return created_draft_objects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -135,6 +144,9 @@ def do_edit_draft(draft_id: int, draft_dict: Dict[str, Any], user_profile: UserP
 | 
				
			|||||||
    draft_object.last_edit_time = valid_draft_dict["last_edit_time"]
 | 
					    draft_object.last_edit_time = valid_draft_dict["last_edit_time"]
 | 
				
			||||||
    draft_object.save()
 | 
					    draft_object.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    event = {"type": "drafts", "op": "update", "draft": draft_object.to_dict()}
 | 
				
			||||||
 | 
					    send_event(user_profile.realm, event, [user_profile.id])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def do_delete_draft(draft_id: int, user_profile: UserProfile) -> None:
 | 
					def do_delete_draft(draft_id: int, user_profile: UserProfile) -> None:
 | 
				
			||||||
    """Delete a draft belonging to a particular user."""
 | 
					    """Delete a draft belonging to a particular user."""
 | 
				
			||||||
@@ -142,4 +154,9 @@ def do_delete_draft(draft_id: int, user_profile: UserProfile) -> None:
 | 
				
			|||||||
        draft_object = Draft.objects.get(id=draft_id, user_profile=user_profile)
 | 
					        draft_object = Draft.objects.get(id=draft_id, user_profile=user_profile)
 | 
				
			||||||
    except Draft.DoesNotExist:
 | 
					    except Draft.DoesNotExist:
 | 
				
			||||||
        raise ResourceNotFoundError(_("Draft does not exist"))
 | 
					        raise ResourceNotFoundError(_("Draft does not exist"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    draft_id = draft_object.id
 | 
				
			||||||
    draft_object.delete()
 | 
					    draft_object.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    event = {"type": "drafts", "op": "remove", "draft_id": draft_id}
 | 
				
			||||||
 | 
					    send_event(user_profile.realm, event, [user_profile.id])
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -54,6 +54,7 @@ from zerver.models import (
 | 
				
			|||||||
    MAX_TOPIC_NAME_LENGTH,
 | 
					    MAX_TOPIC_NAME_LENGTH,
 | 
				
			||||||
    Client,
 | 
					    Client,
 | 
				
			||||||
    CustomProfileField,
 | 
					    CustomProfileField,
 | 
				
			||||||
 | 
					    Draft,
 | 
				
			||||||
    Message,
 | 
					    Message,
 | 
				
			||||||
    Realm,
 | 
					    Realm,
 | 
				
			||||||
    Stream,
 | 
					    Stream,
 | 
				
			||||||
@@ -169,6 +170,17 @@ def fetch_initial_state_data(
 | 
				
			|||||||
        else:
 | 
					        else:
 | 
				
			||||||
            state["max_message_id"] = -1
 | 
					            state["max_message_id"] = -1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if want("drafts"):
 | 
				
			||||||
 | 
					        # Note: if a user ever disables synching drafts then all of
 | 
				
			||||||
 | 
					        # their old drafts stored on the server will be deleted and
 | 
				
			||||||
 | 
					        # simply retained in local storage. In which case user_drafts
 | 
				
			||||||
 | 
					        # would just be an empty queryset.
 | 
				
			||||||
 | 
					        user_draft_objects = Draft.objects.filter(user_profile=user_profile).order_by(
 | 
				
			||||||
 | 
					            "-last_edit_time"
 | 
				
			||||||
 | 
					        )[: settings.MAX_DRAFTS_IN_REGISTER_RESPONSE]
 | 
				
			||||||
 | 
					        user_draft_dicts = [draft.to_dict() for draft in user_draft_objects]
 | 
				
			||||||
 | 
					        state["drafts"] = user_draft_dicts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if want("muted_topics"):
 | 
					    if want("muted_topics"):
 | 
				
			||||||
        state["muted_topics"] = [] if user_profile is None else get_topic_mutes(user_profile)
 | 
					        state["muted_topics"] = [] if user_profile is None else get_topic_mutes(user_profile)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -618,6 +630,34 @@ def apply_event(
 | 
				
			|||||||
        # It may be impossible for a heartbeat event to actually reach
 | 
					        # It may be impossible for a heartbeat event to actually reach
 | 
				
			||||||
        # this code path. But in any case, they're noops.
 | 
					        # this code path. But in any case, they're noops.
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    elif event["type"] == "drafts":
 | 
				
			||||||
 | 
					        if event["op"] == "add":
 | 
				
			||||||
 | 
					            state["drafts"].extend(event["drafts"])
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            if event["op"] == "update":
 | 
				
			||||||
 | 
					                event_draft_idx = event["draft"]["id"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                def _draft_update_action(i: int) -> None:
 | 
				
			||||||
 | 
					                    state["drafts"][i] = event["draft"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            elif event["op"] == "remove":
 | 
				
			||||||
 | 
					                event_draft_idx = event["draft_id"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                def _draft_update_action(i: int) -> None:
 | 
				
			||||||
 | 
					                    del state["drafts"][i]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # We have to perform a linear search for the draft that
 | 
				
			||||||
 | 
					            # was either edited or removed since we have a list
 | 
				
			||||||
 | 
					            # ordered by the last edited timestamp and not id.
 | 
				
			||||||
 | 
					            state_draft_idx = None
 | 
				
			||||||
 | 
					            for idx, draft in enumerate(state["drafts"]):
 | 
				
			||||||
 | 
					                if draft["id"] == event_draft_idx:
 | 
				
			||||||
 | 
					                    state_draft_idx = idx
 | 
				
			||||||
 | 
					                    break
 | 
				
			||||||
 | 
					            assert state_draft_idx is not None
 | 
				
			||||||
 | 
					            _draft_update_action(state_draft_idx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    elif event["type"] == "hotspots":
 | 
					    elif event["type"] == "hotspots":
 | 
				
			||||||
        state["hotspots"] = event["hotspots"]
 | 
					        state["hotspots"] = event["hotspots"]
 | 
				
			||||||
    elif event["type"] == "custom_profile_fields":
 | 
					    elif event["type"] == "custom_profile_fields":
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3818,6 +3818,102 @@ paths:
 | 
				
			|||||||
                                    },
 | 
					                                    },
 | 
				
			||||||
                                  "id": 0,
 | 
					                                  "id": 0,
 | 
				
			||||||
                                }
 | 
					                                }
 | 
				
			||||||
 | 
					                            - type: object
 | 
				
			||||||
 | 
					                              additionalProperties: false
 | 
				
			||||||
 | 
					                              description: |
 | 
				
			||||||
 | 
					                                Event containing details of newly created drafts.
 | 
				
			||||||
 | 
					                              properties:
 | 
				
			||||||
 | 
					                                id:
 | 
				
			||||||
 | 
					                                  $ref: "#/components/schemas/EventIdSchema"
 | 
				
			||||||
 | 
					                                type:
 | 
				
			||||||
 | 
					                                  allOf:
 | 
				
			||||||
 | 
					                                    - $ref: "#/components/schemas/EventTypeSchema"
 | 
				
			||||||
 | 
					                                    - enum:
 | 
				
			||||||
 | 
					                                        - "drafts"
 | 
				
			||||||
 | 
					                                op:
 | 
				
			||||||
 | 
					                                  type: string
 | 
				
			||||||
 | 
					                                  enum:
 | 
				
			||||||
 | 
					                                    - "add"
 | 
				
			||||||
 | 
					                                drafts:
 | 
				
			||||||
 | 
					                                  type: array
 | 
				
			||||||
 | 
					                                  description: |
 | 
				
			||||||
 | 
					                                    An array containing objects for the newly created drafts.
 | 
				
			||||||
 | 
					                                  items:
 | 
				
			||||||
 | 
					                                    $ref: "#/components/schemas/Draft"
 | 
				
			||||||
 | 
					                              example:
 | 
				
			||||||
 | 
					                                {
 | 
				
			||||||
 | 
					                                  "type": "drafts",
 | 
				
			||||||
 | 
					                                  "op": "add",
 | 
				
			||||||
 | 
					                                  "drafts":
 | 
				
			||||||
 | 
					                                    [
 | 
				
			||||||
 | 
					                                      {
 | 
				
			||||||
 | 
					                                        "id": 17,
 | 
				
			||||||
 | 
					                                        "type": "private",
 | 
				
			||||||
 | 
					                                        "to": [6],
 | 
				
			||||||
 | 
					                                        "topic": "",
 | 
				
			||||||
 | 
					                                        "content": "Hello there!",
 | 
				
			||||||
 | 
					                                        "timestamp": 15954790200,
 | 
				
			||||||
 | 
					                                      },
 | 
				
			||||||
 | 
					                                    ],
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            - type: object
 | 
				
			||||||
 | 
					                              additionalProperties: false
 | 
				
			||||||
 | 
					                              description: |
 | 
				
			||||||
 | 
					                                Event containing details for an edited draft.
 | 
				
			||||||
 | 
					                              properties:
 | 
				
			||||||
 | 
					                                id:
 | 
				
			||||||
 | 
					                                  $ref: "#/components/schemas/EventIdSchema"
 | 
				
			||||||
 | 
					                                type:
 | 
				
			||||||
 | 
					                                  allOf:
 | 
				
			||||||
 | 
					                                    - $ref: "#/components/schemas/EventTypeSchema"
 | 
				
			||||||
 | 
					                                    - enum:
 | 
				
			||||||
 | 
					                                        - "drafts"
 | 
				
			||||||
 | 
					                                op:
 | 
				
			||||||
 | 
					                                  type: string
 | 
				
			||||||
 | 
					                                  enum:
 | 
				
			||||||
 | 
					                                    - "update"
 | 
				
			||||||
 | 
					                                draft:
 | 
				
			||||||
 | 
					                                  $ref: "#/components/schemas/Draft"
 | 
				
			||||||
 | 
					                              example:
 | 
				
			||||||
 | 
					                                {
 | 
				
			||||||
 | 
					                                  "type": "drafts",
 | 
				
			||||||
 | 
					                                  "op": "update",
 | 
				
			||||||
 | 
					                                  "draft":
 | 
				
			||||||
 | 
					                                    {
 | 
				
			||||||
 | 
					                                      "id": 17,
 | 
				
			||||||
 | 
					                                      "type": "private",
 | 
				
			||||||
 | 
					                                      "to": [6, 7, 8, 9, 10],
 | 
				
			||||||
 | 
					                                      "topic": "",
 | 
				
			||||||
 | 
					                                      "content": "Hello everyone!",
 | 
				
			||||||
 | 
					                                      "timestamp": 15954790200,
 | 
				
			||||||
 | 
					                                    },
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            - type: object
 | 
				
			||||||
 | 
					                              additionalProperties: false
 | 
				
			||||||
 | 
					                              description: |
 | 
				
			||||||
 | 
					                                Event containing the id of a deleted draft.
 | 
				
			||||||
 | 
					                              properties:
 | 
				
			||||||
 | 
					                                id:
 | 
				
			||||||
 | 
					                                  $ref: "#/components/schemas/EventIdSchema"
 | 
				
			||||||
 | 
					                                type:
 | 
				
			||||||
 | 
					                                  allOf:
 | 
				
			||||||
 | 
					                                    - $ref: "#/components/schemas/EventTypeSchema"
 | 
				
			||||||
 | 
					                                    - enum:
 | 
				
			||||||
 | 
					                                        - "drafts"
 | 
				
			||||||
 | 
					                                op:
 | 
				
			||||||
 | 
					                                  type: string
 | 
				
			||||||
 | 
					                                  enum:
 | 
				
			||||||
 | 
					                                    - "remove"
 | 
				
			||||||
 | 
					                                draft_id:
 | 
				
			||||||
 | 
					                                  type: integer
 | 
				
			||||||
 | 
					                                  description: |
 | 
				
			||||||
 | 
					                                    The ID of the draft that was just deleted.
 | 
				
			||||||
 | 
					                              example:
 | 
				
			||||||
 | 
					                                {
 | 
				
			||||||
 | 
					                                  "type": "drafts",
 | 
				
			||||||
 | 
					                                  "op": "update",
 | 
				
			||||||
 | 
					                                  "draft_id": 17,
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
                      queue_id:
 | 
					                      queue_id:
 | 
				
			||||||
                        type: string
 | 
					                        type: string
 | 
				
			||||||
                        description: |
 | 
					                        description: |
 | 
				
			||||||
@@ -7980,6 +8076,14 @@ paths:
 | 
				
			|||||||
                              type: string
 | 
					                              type: string
 | 
				
			||||||
                              description: |
 | 
					                              description: |
 | 
				
			||||||
                                The name of the custom profile field type.
 | 
					                                The name of the custom profile field type.
 | 
				
			||||||
 | 
					                      drafts:
 | 
				
			||||||
 | 
					                        type: array
 | 
				
			||||||
 | 
					                        description: |
 | 
				
			||||||
 | 
					                          An array containing draft objects for the user. These drafts are being
 | 
				
			||||||
 | 
					                          stored on the backend for the purpose of syncing across devices. This
 | 
				
			||||||
 | 
					                          array will be empty if `enable_drafts_synchronization` is set to `false`.
 | 
				
			||||||
 | 
					                        items:
 | 
				
			||||||
 | 
					                          $ref: "#/components/schemas/Draft"
 | 
				
			||||||
                      hotspots:
 | 
					                      hotspots:
 | 
				
			||||||
                        type: array
 | 
					                        type: array
 | 
				
			||||||
                        description: |
 | 
					                        description: |
 | 
				
			||||||
@@ -8835,7 +8939,8 @@ paths:
 | 
				
			|||||||
                        description: |
 | 
					                        description: |
 | 
				
			||||||
                          Present if `update_display_settings` is present in `fetch_event_types`.
 | 
					                          Present if `update_display_settings` is present in `fetch_event_types`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                          Whether drafts synchronization is enabled for the user.
 | 
					                          Whether drafts synchronization is enabled for the user. If disabled,
 | 
				
			||||||
 | 
					                          clients will receive an error when trying to use the `drafts` endpoints.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                          See [PATCH /settings](/api/update-settings) for details on
 | 
					                          See [PATCH /settings](/api/update-settings) for details on
 | 
				
			||||||
                          the meaning of this setting.
 | 
					                          the meaning of this setting.
 | 
				
			||||||
@@ -12800,6 +12905,59 @@ components:
 | 
				
			|||||||
          description: |
 | 
					          description: |
 | 
				
			||||||
            Whether the client is capable of showing mobile/push notifications
 | 
					            Whether the client is capable of showing mobile/push notifications
 | 
				
			||||||
            to the user.
 | 
					            to the user.
 | 
				
			||||||
 | 
					    Draft:
 | 
				
			||||||
 | 
					      type: object
 | 
				
			||||||
 | 
					      description: |
 | 
				
			||||||
 | 
					        A dictionary for representing a message draft.
 | 
				
			||||||
 | 
					      properties:
 | 
				
			||||||
 | 
					        id:
 | 
				
			||||||
 | 
					          type: integer
 | 
				
			||||||
 | 
					          description: |
 | 
				
			||||||
 | 
					            The unique ID of the draft. It will only used whenever the drafts are
 | 
				
			||||||
 | 
					            fetched. This field should not be specified when the draft is being
 | 
				
			||||||
 | 
					            created or edited.
 | 
				
			||||||
 | 
					        type:
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					          description: |
 | 
				
			||||||
 | 
					            The type of the draft. Either unaddressed (empty string), "stream",
 | 
				
			||||||
 | 
					            or "private" (for PMs and private group messages).
 | 
				
			||||||
 | 
					          enum:
 | 
				
			||||||
 | 
					            - ""
 | 
				
			||||||
 | 
					            - "stream"
 | 
				
			||||||
 | 
					            - "private"
 | 
				
			||||||
 | 
					        to:
 | 
				
			||||||
 | 
					          type: array
 | 
				
			||||||
 | 
					          description: |
 | 
				
			||||||
 | 
					            An array of the tentative target audience IDs. For "stream"
 | 
				
			||||||
 | 
					            messages, this should contain exactly 1 ID, the ID of the
 | 
				
			||||||
 | 
					            target stream. For private messages, this should be an array
 | 
				
			||||||
 | 
					            of target user IDs. For unaddressed drafts this is ignored
 | 
				
			||||||
 | 
					            so it's best to leave it as an empty array.
 | 
				
			||||||
 | 
					          items:
 | 
				
			||||||
 | 
					            type: integer
 | 
				
			||||||
 | 
					        topic:
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					          description: |
 | 
				
			||||||
 | 
					            For stream message drafts, the tentative topic name. For private
 | 
				
			||||||
 | 
					            or unaddressed messages this will be ignored and should ideally
 | 
				
			||||||
 | 
					            be an empty string. Should not contain null bytes.
 | 
				
			||||||
 | 
					        content:
 | 
				
			||||||
 | 
					          type: string
 | 
				
			||||||
 | 
					          description: |
 | 
				
			||||||
 | 
					            The body of the draft. Should not contain null bytes.
 | 
				
			||||||
 | 
					        timestamp:
 | 
				
			||||||
 | 
					          type: number
 | 
				
			||||||
 | 
					          description: |
 | 
				
			||||||
 | 
					            A Unix timestamp (seconds only) representing when the draft was
 | 
				
			||||||
 | 
					            last edited. When creating a draft, this key need not be present
 | 
				
			||||||
 | 
					            and it will be filled in automatically by the server.
 | 
				
			||||||
 | 
					          example: 1595479019
 | 
				
			||||||
 | 
					      additionalProperties: false
 | 
				
			||||||
 | 
					      required:
 | 
				
			||||||
 | 
					        - type
 | 
				
			||||||
 | 
					        - to
 | 
				
			||||||
 | 
					        - topic
 | 
				
			||||||
 | 
					        - content
 | 
				
			||||||
    User:
 | 
					    User:
 | 
				
			||||||
      allOf:
 | 
					      allOf:
 | 
				
			||||||
        - $ref: "#/components/schemas/UserBase"
 | 
					        - $ref: "#/components/schemas/UserBase"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -980,13 +980,14 @@ class FetchQueriesTest(ZulipTestCase):
 | 
				
			|||||||
            with mock.patch("zerver.lib.events.always_want") as want_mock:
 | 
					            with mock.patch("zerver.lib.events.always_want") as want_mock:
 | 
				
			||||||
                fetch_initial_state_data(user)
 | 
					                fetch_initial_state_data(user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assert_length(queries, 33)
 | 
					        self.assert_length(queries, 34)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        expected_counts = dict(
 | 
					        expected_counts = dict(
 | 
				
			||||||
            alert_words=1,
 | 
					            alert_words=1,
 | 
				
			||||||
            custom_profile_fields=1,
 | 
					            custom_profile_fields=1,
 | 
				
			||||||
            default_streams=1,
 | 
					            default_streams=1,
 | 
				
			||||||
            default_stream_groups=1,
 | 
					            default_stream_groups=1,
 | 
				
			||||||
 | 
					            drafts=1,
 | 
				
			||||||
            hotspots=0,
 | 
					            hotspots=0,
 | 
				
			||||||
            message=1,
 | 
					            message=1,
 | 
				
			||||||
            muted_topics=1,
 | 
					            muted_topics=1,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -101,6 +101,7 @@ from zerver.lib.actions import (
 | 
				
			|||||||
    try_add_realm_custom_profile_field,
 | 
					    try_add_realm_custom_profile_field,
 | 
				
			||||||
    try_update_realm_custom_profile_field,
 | 
					    try_update_realm_custom_profile_field,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					from zerver.lib.drafts import do_create_drafts, do_delete_draft, do_edit_draft
 | 
				
			||||||
from zerver.lib.event_schema import (
 | 
					from zerver.lib.event_schema import (
 | 
				
			||||||
    check_alert_words,
 | 
					    check_alert_words,
 | 
				
			||||||
    check_attachment_add,
 | 
					    check_attachment_add,
 | 
				
			||||||
@@ -2308,6 +2309,45 @@ class DraftActionTest(BaseAction):
 | 
				
			|||||||
    def do_disable_drafts_synchronization(self, user_profile: UserProfile) -> None:
 | 
					    def do_disable_drafts_synchronization(self, user_profile: UserProfile) -> None:
 | 
				
			||||||
        do_set_user_display_setting(user_profile, "enable_drafts_synchronization", False)
 | 
					        do_set_user_display_setting(user_profile, "enable_drafts_synchronization", False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_draft_create_event(self) -> None:
 | 
				
			||||||
 | 
					        self.do_enable_drafts_synchronization(self.user_profile)
 | 
				
			||||||
 | 
					        dummy_draft = {
 | 
				
			||||||
 | 
					            "type": "draft",
 | 
				
			||||||
 | 
					            "to": "",
 | 
				
			||||||
 | 
					            "topic": "",
 | 
				
			||||||
 | 
					            "content": "Sample draft content",
 | 
				
			||||||
 | 
					            "timestamp": 1596820995,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        action = lambda: do_create_drafts([dummy_draft], self.user_profile)
 | 
				
			||||||
 | 
					        self.verify_action(action)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_draft_edit_event(self) -> None:
 | 
				
			||||||
 | 
					        self.do_enable_drafts_synchronization(self.user_profile)
 | 
				
			||||||
 | 
					        dummy_draft = {
 | 
				
			||||||
 | 
					            "type": "draft",
 | 
				
			||||||
 | 
					            "to": "",
 | 
				
			||||||
 | 
					            "topic": "",
 | 
				
			||||||
 | 
					            "content": "Sample draft content",
 | 
				
			||||||
 | 
					            "timestamp": 1596820995,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        draft_id = do_create_drafts([dummy_draft], self.user_profile)[0].id
 | 
				
			||||||
 | 
					        dummy_draft["content"] = "Some more sample draft content"
 | 
				
			||||||
 | 
					        action = lambda: do_edit_draft(draft_id, dummy_draft, self.user_profile)
 | 
				
			||||||
 | 
					        self.verify_action(action)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_draft_delete_event(self) -> None:
 | 
				
			||||||
 | 
					        self.do_enable_drafts_synchronization(self.user_profile)
 | 
				
			||||||
 | 
					        dummy_draft = {
 | 
				
			||||||
 | 
					            "type": "draft",
 | 
				
			||||||
 | 
					            "to": "",
 | 
				
			||||||
 | 
					            "topic": "",
 | 
				
			||||||
 | 
					            "content": "Sample draft content",
 | 
				
			||||||
 | 
					            "timestamp": 1596820995,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        draft_id = do_create_drafts([dummy_draft], self.user_profile)[0].id
 | 
				
			||||||
 | 
					        action = lambda: do_delete_draft(draft_id, self.user_profile)
 | 
				
			||||||
 | 
					        self.verify_action(action)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_enable_syncing_drafts(self) -> None:
 | 
					    def test_enable_syncing_drafts(self) -> None:
 | 
				
			||||||
        self.do_disable_drafts_synchronization(self.user_profile)
 | 
					        self.do_disable_drafts_synchronization(self.user_profile)
 | 
				
			||||||
        action = lambda: self.do_enable_drafts_synchronization(self.user_profile)
 | 
					        action = lambda: self.do_enable_drafts_synchronization(self.user_profile)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,6 +24,7 @@ from zerver.lib.test_classes import ZulipTestCase
 | 
				
			|||||||
from zerver.lib.test_helpers import get_user_messages, queries_captured
 | 
					from zerver.lib.test_helpers import get_user_messages, queries_captured
 | 
				
			||||||
from zerver.models import (
 | 
					from zerver.models import (
 | 
				
			||||||
    DefaultStream,
 | 
					    DefaultStream,
 | 
				
			||||||
 | 
					    Draft,
 | 
				
			||||||
    Realm,
 | 
					    Realm,
 | 
				
			||||||
    UserActivity,
 | 
					    UserActivity,
 | 
				
			||||||
    UserProfile,
 | 
					    UserProfile,
 | 
				
			||||||
@@ -64,6 +65,7 @@ class HomeTest(ZulipTestCase):
 | 
				
			|||||||
        "dense_mode",
 | 
					        "dense_mode",
 | 
				
			||||||
        "desktop_icon_count_display",
 | 
					        "desktop_icon_count_display",
 | 
				
			||||||
        "development_environment",
 | 
					        "development_environment",
 | 
				
			||||||
 | 
					        "drafts",
 | 
				
			||||||
        "email",
 | 
					        "email",
 | 
				
			||||||
        "email_notifications_batching_period_seconds",
 | 
					        "email_notifications_batching_period_seconds",
 | 
				
			||||||
        "emojiset",
 | 
					        "emojiset",
 | 
				
			||||||
@@ -271,7 +273,7 @@ class HomeTest(ZulipTestCase):
 | 
				
			|||||||
            set(result["Cache-Control"].split(", ")), {"must-revalidate", "no-store", "no-cache"}
 | 
					            set(result["Cache-Control"].split(", ")), {"must-revalidate", "no-store", "no-cache"}
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assert_length(queries, 42)
 | 
					        self.assert_length(queries, 43)
 | 
				
			||||||
        self.assert_length(cache_mock.call_args_list, 5)
 | 
					        self.assert_length(cache_mock.call_args_list, 5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        html = result.content.decode("utf-8")
 | 
					        html = result.content.decode("utf-8")
 | 
				
			||||||
@@ -351,7 +353,7 @@ class HomeTest(ZulipTestCase):
 | 
				
			|||||||
                result = self._get_home_page()
 | 
					                result = self._get_home_page()
 | 
				
			||||||
                self.check_rendered_logged_in_app(result)
 | 
					                self.check_rendered_logged_in_app(result)
 | 
				
			||||||
                self.assert_length(cache_mock.call_args_list, 6)
 | 
					                self.assert_length(cache_mock.call_args_list, 6)
 | 
				
			||||||
            self.assert_length(queries, 39)
 | 
					            self.assert_length(queries, 40)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_num_queries_with_streams(self) -> None:
 | 
					    def test_num_queries_with_streams(self) -> None:
 | 
				
			||||||
        main_user = self.example_user("hamlet")
 | 
					        main_user = self.example_user("hamlet")
 | 
				
			||||||
@@ -382,7 +384,7 @@ class HomeTest(ZulipTestCase):
 | 
				
			|||||||
        with queries_captured() as queries2:
 | 
					        with queries_captured() as queries2:
 | 
				
			||||||
            result = self._get_home_page()
 | 
					            result = self._get_home_page()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assert_length(queries2, 37)
 | 
					        self.assert_length(queries2, 38)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Do a sanity check that our new streams were in the payload.
 | 
					        # Do a sanity check that our new streams were in the payload.
 | 
				
			||||||
        html = result.content.decode("utf-8")
 | 
					        html = result.content.decode("utf-8")
 | 
				
			||||||
@@ -1041,3 +1043,35 @@ class HomeTest(ZulipTestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        page_params = self._get_page_params(result)
 | 
					        page_params = self._get_page_params(result)
 | 
				
			||||||
        self.assertEqual(page_params["default_language"], "es")
 | 
					        self.assertEqual(page_params["default_language"], "es")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @override_settings(MAX_DRAFTS_IN_REGISTER_RESPONSE=5)
 | 
				
			||||||
 | 
					    def test_limit_drafts(self) -> None:
 | 
				
			||||||
 | 
					        draft_objects = []
 | 
				
			||||||
 | 
					        hamlet = self.example_user("hamlet")
 | 
				
			||||||
 | 
					        base_time = timezone_now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        step_value = timedelta(seconds=1)
 | 
				
			||||||
 | 
					        # Create 11 drafts.
 | 
				
			||||||
 | 
					        for i in range(0, settings.MAX_DRAFTS_IN_REGISTER_RESPONSE + 1):
 | 
				
			||||||
 | 
					            draft_objects.append(
 | 
				
			||||||
 | 
					                Draft(
 | 
				
			||||||
 | 
					                    user_profile=hamlet,
 | 
				
			||||||
 | 
					                    recipient=None,
 | 
				
			||||||
 | 
					                    topic="",
 | 
				
			||||||
 | 
					                    content="sample draft",
 | 
				
			||||||
 | 
					                    last_edit_time=base_time + i * step_value,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        Draft.objects.bulk_create(draft_objects)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Now fetch the drafts part of the initial state and make sure
 | 
				
			||||||
 | 
					        # that we only got back settings.MAX_DRAFTS_IN_REGISTER_RESPONSE.
 | 
				
			||||||
 | 
					        # No more. Also make sure that the drafts returned are the most
 | 
				
			||||||
 | 
					        # recently edited ones.
 | 
				
			||||||
 | 
					        self.login("hamlet")
 | 
				
			||||||
 | 
					        page_params = self._get_page_params(self._get_home_page())
 | 
				
			||||||
 | 
					        self.assertEqual(page_params["enable_drafts_synchronization"], True)
 | 
				
			||||||
 | 
					        self.assert_length(page_params["drafts"], settings.MAX_DRAFTS_IN_REGISTER_RESPONSE)
 | 
				
			||||||
 | 
					        self.assertEqual(Draft.objects.count(), settings.MAX_DRAFTS_IN_REGISTER_RESPONSE + 1)
 | 
				
			||||||
 | 
					        for draft in page_params["drafts"]:
 | 
				
			||||||
 | 
					            self.assertNotEqual(draft["timestamp"], base_time)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -455,3 +455,8 @@ OUTGOING_WEBHOOK_TIMEOUT_SECONDS = 10
 | 
				
			|||||||
# Any message content exceeding this limit will be truncated.
 | 
					# Any message content exceeding this limit will be truncated.
 | 
				
			||||||
# See: `_internal_prep_message` function in zerver/lib/actions.py.
 | 
					# See: `_internal_prep_message` function in zerver/lib/actions.py.
 | 
				
			||||||
MAX_MESSAGE_LENGTH = 10000
 | 
					MAX_MESSAGE_LENGTH = 10000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# The maximum number of drafts to send in the response to /register.
 | 
				
			||||||
 | 
					# More drafts, should they exist for some crazy reason, could be
 | 
				
			||||||
 | 
					# fetched in a separate request.
 | 
				
			||||||
 | 
					MAX_DRAFTS_IN_REGISTER_RESPONSE = 1000
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user