From d8a8364d1a662ce8d6c7fe0e02d1625b250b1979 Mon Sep 17 00:00:00 2001 From: N-Shar-ma Date: Thu, 4 Aug 2022 19:50:54 +0530 Subject: [PATCH] todo_widget: Allow tasks to be added through `/todo` command. Uptil now, users could add tasks to a todo widget only after creating it through the `/todo` command in the compose box. Users can now add an initial list of tasks using the `/todo` command, with each task on a new line in the compose box, where the 1st `:` would separate a task from its (optional) description. Example: `/todo\nTask1:description1\nTask2 without description`. Fixes part of #20213. --- web/src/submessage.ts | 1 + web/src/todo_widget.js | 12 +++++- web/src/widgetize.ts | 1 + zerver/lib/widget.py | 30 ++----------- zerver/tests/test_widgets.py | 82 ++++++++++++++++++++++++++++++++---- 5 files changed, 89 insertions(+), 37 deletions(-) diff --git a/web/src/submessage.ts b/web/src/submessage.ts index 40c1afa50b..a27c3fea33 100644 --- a/web/src/submessage.ts +++ b/web/src/submessage.ts @@ -32,6 +32,7 @@ const poll_widget_extra_data_schema = z export const todo_widget_extra_data_schema = z .object({ task_list_title: z.string().optional(), + tasks: z.array(z.object({task: z.string(), desc: z.string()})).optional(), }) .nullable(); diff --git a/web/src/todo_widget.js b/web/src/todo_widget.js index 757358811f..77041b4a76 100644 --- a/web/src/todo_widget.js +++ b/web/src/todo_widget.js @@ -22,6 +22,7 @@ export class TaskData { current_user_id, is_my_task_list, task_list_title, + tasks, report_error_function, }) { this.message_sender_id = message_sender_id; @@ -36,6 +37,14 @@ export class TaskData { } else { this.set_task_list_title($t({defaultMessage: "Task list"})); } + + for (const [i, data] of tasks.entries()) { + this.handle.new_task.inbound("canned", { + key: i, + task: data.task, + desc: data.desc, + }); + } } set_task_list_title(new_title) { @@ -219,13 +228,14 @@ export function activate({$elem, callback, extra_data, message}) { return; } const {data} = parse_result; - const {task_list_title = ""} = data || {}; + const {task_list_title = "", tasks = []} = data || {}; const is_my_task_list = people.is_my_user_id(message.sender_id); const task_data = new TaskData({ message_sender_id: message.sender_id, current_user_id: people.my_current_user_id(), is_my_task_list, task_list_title, + tasks, report_error_function: blueslip.warn, }); diff --git a/web/src/widgetize.ts b/web/src/widgetize.ts index 65ef7c8501..349c566c00 100644 --- a/web/src/widgetize.ts +++ b/web/src/widgetize.ts @@ -15,6 +15,7 @@ type ZFormExtraData = { // TODO: This TodoWidgetExtraData type should be moved to web/src/todo_widget.js when it will be migrated type TodoWidgetExtraData = { task_list_title?: string; + tasks?: {task: string; desc: string}[]; }; type WidgetExtraData = PollWidgetExtraData | TodoWidgetExtraData | ZFormExtraData | null; diff --git a/zerver/lib/widget.py b/zerver/lib/widget.py index a0e3ff20a4..0e2ee2281c 100644 --- a/zerver/lib/widget.py +++ b/zerver/lib/widget.py @@ -6,7 +6,7 @@ from zerver.lib.message import SendMessageRequest from zerver.models import Message, SubMessage -def get_widget_data(content: str) -> Tuple[Optional[str], Optional[str]]: +def get_widget_data(content: str) -> Tuple[Optional[str], Any]: valid_widget_types = ["poll", "todo"] tokens = re.split(r"\s+|\n+", content) @@ -73,33 +73,9 @@ def parse_todo_extra_data(content: str) -> Any: def get_extra_data_from_widget_type(content: str, widget_type: Optional[str]) -> Any: if widget_type == "poll": - # This is used to extract the question from the poll command. - # The command '/poll question' will pre-set the question in the poll - lines = content.splitlines() - question = "" - options = [] - if lines and lines[0]: - question = lines.pop(0).strip() - for line in lines: - # If someone is using the list syntax, we remove it - # before adding an option. - option = re.sub(r"(\s*[-*]?\s*)", "", line.strip(), count=1) - if len(option) > 0: - options.append(option) - extra_data = { - "question": question, - "options": options, - } - return extra_data + return parse_poll_extra_data(content) else: - # This is used to extract the task list title from the todo command. - # The command '/todo Title' will pre-set the task list title - lines = content.splitlines() - task_list_title = "" - if lines and lines[0]: - task_list_title = lines.pop(0).strip() - extra_data = {"task_list_title": task_list_title} - return extra_data + return parse_todo_extra_data(content) def do_widget_post_save_actions(send_request: SendMessageRequest) -> None: diff --git a/zerver/tests/test_widgets.py b/zerver/tests/test_widgets.py index 9a02ae6404..39990d2b41 100644 --- a/zerver/tests/test_widgets.py +++ b/zerver/tests/test_widgets.py @@ -98,13 +98,17 @@ class WidgetContentTestCase(ZulipTestCase): self.assertEqual(get_widget_data(content=message), (None, None)) # Add positive checks for context - self.assertEqual(get_widget_data(content="/todo"), ("todo", {"task_list_title": ""})) self.assertEqual( - get_widget_data(content="/todo Title"), ("todo", {"task_list_title": "Title"}) + get_widget_data(content="/todo"), ("todo", {"task_list_title": "", "tasks": []}) + ) + self.assertEqual( + get_widget_data(content="/todo Title"), + ("todo", {"task_list_title": "Title", "tasks": []}), ) # Test tokenization on newline character self.assertEqual( - get_widget_data(content="/todo\nignore"), ("todo", {"task_list_title": ""}) + get_widget_data(content="/todo\nTask"), + ("todo", {"task_list_title": "", "tasks": [{"task": "Task", "desc": ""}]}), ) def test_explicit_widget_content(self) -> None: @@ -171,7 +175,7 @@ class WidgetContentTestCase(ZulipTestCase): expected_submessage_content = dict( widget_type="todo", - extra_data={"task_list_title": ""}, + extra_data={"task_list_title": "", "tasks": []}, ) submessage = SubMessage.objects.get(message_id=message.id) @@ -188,7 +192,42 @@ class WidgetContentTestCase(ZulipTestCase): expected_submessage_content = dict( widget_type="todo", - extra_data={"task_list_title": "Example Task List Title"}, + extra_data={"task_list_title": "Example Task List Title", "tasks": []}, + ) + + submessage = SubMessage.objects.get(message_id=message.id) + self.assertEqual(submessage.msg_type, "widget") + self.assertEqual(orjson.loads(submessage.content), expected_submessage_content) + + # We test for both trailing and leading spaces, along with blank lines + # for the tasks. + content = "/todo Example Task List Title\n\n task without description\ntask: with description \n\n - task as list : also with description" + payload["content"] = content + result = self.api_post(sender, "/api/v1/messages", payload) + self.assert_json_success(result) + + message = self.get_last_message() + self.assertEqual(message.content, content) + + expected_submessage_content = dict( + widget_type="todo", + extra_data=dict( + task_list_title="Example Task List Title", + tasks=[ + dict( + task="task without description", + desc="", + ), + dict( + task="task", + desc="with description", + ), + dict( + task="task as list", + desc="also with description", + ), + ], + ), ) submessage = SubMessage.objects.get(message_id=message.id) @@ -267,9 +306,7 @@ class WidgetContentTestCase(ZulipTestCase): expected_submessage_content = dict( widget_type="todo", - extra_data=dict( - task_list_title="School Work", - ), + extra_data=dict(task_list_title="School Work", tasks=[]), ) submessage = SubMessage.objects.get(message_id=message.id) @@ -283,10 +320,37 @@ class WidgetContentTestCase(ZulipTestCase): result = self.api_post(sender, "/api/v1/messages", payload) self.assert_json_success(result) + expected_submessage_content = dict( + widget_type="todo", + extra_data=dict(task_list_title="", tasks=[]), + ) + + message = self.get_last_message() + self.assertEqual(message.content, content) + submessage = SubMessage.objects.get(message_id=message.id) + self.assertEqual(submessage.msg_type, "widget") + self.assertEqual(orjson.loads(submessage.content), expected_submessage_content) + # Now supply both task list title and tasks. + + content = "/todo School Work\nchemistry homework: assignment 2\nstudy for english test: pages 56-67" + payload["content"] = content + result = self.api_post(sender, "/api/v1/messages", payload) + self.assert_json_success(result) + expected_submessage_content = dict( widget_type="todo", extra_data=dict( - task_list_title="", + task_list_title="School Work", + tasks=[ + dict( + task="chemistry homework", + desc="assignment 2", + ), + dict( + task="study for english test", + desc="pages 56-67", + ), + ], ), )