mirror of https://github.com/zulip/zulip.git
todo_widget: Allow task list title to be set and edited by author.
Users can now name task lists by providing the task list title in the `/todo` command on the same line. Example: `/todo School Work`. If no title is provided by the user, "Task list" (which is also the placeholder) is used as default. The author of a task list can later edit / update the task list title in the todo widget, just like the question in the poll widget. Fixes part of #20213.
This commit is contained in:
parent
b30eb4c4fc
commit
6df3ad251a
|
@ -489,6 +489,7 @@ export const slash_commands = [
|
||||||
text: $t({defaultMessage: "/todo (Create a collaborative to-do list)"}),
|
text: $t({defaultMessage: "/todo (Create a collaborative to-do list)"}),
|
||||||
name: "todo",
|
name: "todo",
|
||||||
aliases: "",
|
aliases: "",
|
||||||
|
placeholder: $t({defaultMessage: "Task list"}),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -29,12 +29,21 @@ const poll_widget_extra_data_schema = z
|
||||||
})
|
})
|
||||||
.nullable();
|
.nullable();
|
||||||
|
|
||||||
|
export const todo_widget_extra_data_schema = z
|
||||||
|
.object({
|
||||||
|
task_list_title: z.string().optional(),
|
||||||
|
})
|
||||||
|
.nullable();
|
||||||
|
|
||||||
const widget_data_event_schema = z.object({
|
const widget_data_event_schema = z.object({
|
||||||
sender_id: z.number(),
|
sender_id: z.number(),
|
||||||
data: z.discriminatedUnion("widget_type", [
|
data: z.discriminatedUnion("widget_type", [
|
||||||
z.object({widget_type: z.literal("poll"), extra_data: poll_widget_extra_data_schema}),
|
z.object({widget_type: z.literal("poll"), extra_data: poll_widget_extra_data_schema}),
|
||||||
z.object({widget_type: z.literal("zform"), extra_data: zform_widget_extra_data_schema}),
|
z.object({widget_type: z.literal("zform"), extra_data: zform_widget_extra_data_schema}),
|
||||||
z.object({widget_type: z.literal("todo"), extra_data: z.null()}),
|
z.object({
|
||||||
|
widget_type: z.literal("todo"),
|
||||||
|
extra_data: todo_widget_extra_data_schema,
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import * as blueslip from "./blueslip";
|
||||||
import {$t} from "./i18n";
|
import {$t} from "./i18n";
|
||||||
import {page_params} from "./page_params";
|
import {page_params} from "./page_params";
|
||||||
import * as people from "./people";
|
import * as people from "./people";
|
||||||
|
import {todo_widget_extra_data_schema} from "./submessage";
|
||||||
|
|
||||||
// Any single user should send add a finite number of tasks
|
// Any single user should send add a finite number of tasks
|
||||||
// to a todo list. We arbitrarily pick this value.
|
// to a todo list. We arbitrarily pick this value.
|
||||||
|
@ -16,8 +17,46 @@ export class TaskData {
|
||||||
task_map = new Map();
|
task_map = new Map();
|
||||||
my_idx = 1;
|
my_idx = 1;
|
||||||
|
|
||||||
constructor({current_user_id}) {
|
constructor({
|
||||||
|
message_sender_id,
|
||||||
|
current_user_id,
|
||||||
|
is_my_task_list,
|
||||||
|
task_list_title,
|
||||||
|
report_error_function,
|
||||||
|
}) {
|
||||||
|
this.message_sender_id = message_sender_id;
|
||||||
this.me = current_user_id;
|
this.me = current_user_id;
|
||||||
|
this.is_my_task_list = is_my_task_list;
|
||||||
|
// input_mode indicates if the task list title is being input currently
|
||||||
|
this.input_mode = is_my_task_list; // for now
|
||||||
|
this.report_error_function = report_error_function;
|
||||||
|
|
||||||
|
if (task_list_title) {
|
||||||
|
this.set_task_list_title(task_list_title);
|
||||||
|
} else {
|
||||||
|
this.set_task_list_title($t({defaultMessage: "Task list"}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_task_list_title(new_title) {
|
||||||
|
this.input_mode = false;
|
||||||
|
this.task_list_title = new_title;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_task_list_title() {
|
||||||
|
return this.task_list_title;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_input_mode() {
|
||||||
|
this.input_mode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_input_mode() {
|
||||||
|
this.input_mode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_input_mode() {
|
||||||
|
return this.input_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
get_widget_data() {
|
get_widget_data() {
|
||||||
|
@ -41,6 +80,36 @@ export class TaskData {
|
||||||
}
|
}
|
||||||
|
|
||||||
handle = {
|
handle = {
|
||||||
|
new_task_list_title: {
|
||||||
|
outbound: (title) => {
|
||||||
|
const event = {
|
||||||
|
type: "new_task_list_title",
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
if (this.is_my_task_list) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
inbound: (sender_id, data) => {
|
||||||
|
// Only the message author can edit questions.
|
||||||
|
if (sender_id !== this.message_sender_id) {
|
||||||
|
this.report_error_function(
|
||||||
|
`user ${sender_id} is not allowed to edit the task list title`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data.title !== "string") {
|
||||||
|
this.report_error_function("todo widget: bad type for inbound task list title");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set_task_list_title(data.title);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
new_task: {
|
new_task: {
|
||||||
outbound: (task, desc) => {
|
outbound: (task, desc) => {
|
||||||
this.my_idx += 1;
|
this.my_idx += 1;
|
||||||
|
@ -143,18 +212,118 @@ export class TaskData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function activate(opts) {
|
export function activate({$elem, callback, extra_data, message}) {
|
||||||
const $elem = opts.$elem;
|
const parse_result = todo_widget_extra_data_schema.safeParse(extra_data);
|
||||||
const callback = opts.callback;
|
if (!parse_result.success) {
|
||||||
|
blueslip.warn("invalid todo extra data", parse_result.error.issues);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {data} = parse_result;
|
||||||
|
const {task_list_title = ""} = data || {};
|
||||||
|
const is_my_task_list = people.is_my_user_id(message.sender_id);
|
||||||
const task_data = new TaskData({
|
const task_data = new TaskData({
|
||||||
|
message_sender_id: message.sender_id,
|
||||||
current_user_id: people.my_current_user_id(),
|
current_user_id: people.my_current_user_id(),
|
||||||
|
is_my_task_list,
|
||||||
|
task_list_title,
|
||||||
|
report_error_function: blueslip.warn,
|
||||||
});
|
});
|
||||||
|
|
||||||
function render() {
|
function update_edit_controls() {
|
||||||
|
const has_title = $elem.find("input.todo-task-list-title").val().trim() !== "";
|
||||||
|
$elem.find("button.todo-task-list-title-check").toggle(has_title);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_task_list_title() {
|
||||||
|
const task_list_title = task_data.get_task_list_title();
|
||||||
|
const input_mode = task_data.get_input_mode();
|
||||||
|
const can_edit = is_my_task_list && !input_mode;
|
||||||
|
|
||||||
|
$elem.find(".todo-task-list-title-header").toggle(!input_mode);
|
||||||
|
$elem.find(".todo-task-list-title-header").text(task_list_title);
|
||||||
|
$elem.find(".todo-edit-task-list-title").toggle(can_edit);
|
||||||
|
update_edit_controls();
|
||||||
|
|
||||||
|
$elem.find(".todo-task-list-title-bar").toggle(input_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function start_editing() {
|
||||||
|
task_data.set_input_mode();
|
||||||
|
|
||||||
|
const task_list_title = task_data.get_task_list_title();
|
||||||
|
$elem.find("input.todo-task-list-title").val(task_list_title);
|
||||||
|
render_task_list_title();
|
||||||
|
$elem.find("input.todo-task-list-title").trigger("focus");
|
||||||
|
}
|
||||||
|
|
||||||
|
function abort_edit() {
|
||||||
|
task_data.clear_input_mode();
|
||||||
|
render_task_list_title();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit_task_list_title() {
|
||||||
|
const $task_list_title_input = $elem.find("input.todo-task-list-title");
|
||||||
|
let new_task_list_title = $task_list_title_input.val().trim();
|
||||||
|
const old_task_list_title = task_data.get_task_list_title();
|
||||||
|
|
||||||
|
// We should disable the button for blank task list title,
|
||||||
|
// so this is just defensive code.
|
||||||
|
if (new_task_list_title.trim() === "") {
|
||||||
|
new_task_list_title = old_task_list_title;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimistically set the task list title locally.
|
||||||
|
task_data.set_task_list_title(new_task_list_title);
|
||||||
|
render_task_list_title();
|
||||||
|
|
||||||
|
// If there were no actual edits, we can exit now.
|
||||||
|
if (new_task_list_title === old_task_list_title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast the new task list title to our peers.
|
||||||
|
const data = task_data.handle.new_task_list_title.outbound(new_task_list_title);
|
||||||
|
callback(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_widget() {
|
||||||
const html = render_widgets_todo_widget();
|
const html = render_widgets_todo_widget();
|
||||||
$elem.html(html);
|
$elem.html(html);
|
||||||
|
|
||||||
|
$elem.find("input.todo-task-list-title").on("keyup", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
update_edit_controls();
|
||||||
|
});
|
||||||
|
|
||||||
|
$elem.find("input.todo-task-list-title").on("keydown", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
submit_task_list_title();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
abort_edit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$elem.find(".todo-edit-task-list-title").on("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
start_editing();
|
||||||
|
});
|
||||||
|
|
||||||
|
$elem.find("button.todo-task-list-title-check").on("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
submit_task_list_title();
|
||||||
|
});
|
||||||
|
|
||||||
|
$elem.find("button.todo-task-list-title-remove").on("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
abort_edit();
|
||||||
|
});
|
||||||
|
|
||||||
$elem.find("button.add-task").on("click", (e) => {
|
$elem.find("button.add-task").on("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
$elem.find(".widget-error").text("");
|
$elem.find(".widget-error").text("");
|
||||||
|
@ -210,9 +379,11 @@ export function activate(opts) {
|
||||||
task_data.handle_event(event.sender_id, event.data);
|
task_data.handle_event(event.sender_id, event.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render_task_list_title();
|
||||||
render_results();
|
render_results();
|
||||||
};
|
};
|
||||||
|
|
||||||
render();
|
build_widget();
|
||||||
|
render_task_list_title();
|
||||||
render_results();
|
render_results();
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,12 @@ type ZFormExtraData = {
|
||||||
choices: {type: string; reply: string; long_name: string; short_name: string}[];
|
choices: {type: string; reply: string; long_name: string; short_name: string}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type WidgetExtraData = PollWidgetExtraData | ZFormExtraData | null;
|
// TODO: This TodoWidgetExtraData type should be moved to web/src/todo_widget.js when it will be migrated
|
||||||
|
type TodoWidgetExtraData = {
|
||||||
|
task_list_title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WidgetExtraData = PollWidgetExtraData | TodoWidgetExtraData | ZFormExtraData | null;
|
||||||
|
|
||||||
type WidgetOptions = {
|
type WidgetOptions = {
|
||||||
widget_type: string;
|
widget_type: string;
|
||||||
|
|
|
@ -216,7 +216,8 @@ button {
|
||||||
&.add-task,
|
&.add-task,
|
||||||
&.add-desc,
|
&.add-desc,
|
||||||
&.poll-option,
|
&.poll-option,
|
||||||
&.poll-question {
|
&.poll-question,
|
||||||
|
&.todo-task-list-title {
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
}
|
}
|
||||||
|
@ -228,7 +229,9 @@ button {
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-question-check,
|
.poll-question-check,
|
||||||
.poll-question-remove {
|
.poll-question-remove,
|
||||||
|
.todo-task-list-title-check,
|
||||||
|
.todo-task-list-title-remove {
|
||||||
width: 28px !important;
|
width: 28px !important;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
@ -240,7 +243,8 @@ button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-edit-question {
|
.poll-edit-question,
|
||||||
|
.todo-edit-task-list-title {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
|
@ -250,7 +254,8 @@ button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.poll-question-header {
|
.poll-question-header,
|
||||||
|
.todo-task-list-title-header {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
<div class="todo-widget">
|
<div class="todo-widget">
|
||||||
<h4>{{t "Task list" }}</h4>
|
<h4 class="todo-task-list-title-header">{{t "Task list" }}</h4>
|
||||||
|
<i class="fa fa-pencil todo-edit-task-list-title"></i>
|
||||||
|
<div class="todo-task-list-title-bar">
|
||||||
|
<input type="text" class="todo-task-list-title" placeholder="{{t 'Add todo task list title'}}" />
|
||||||
|
<button class="todo-task-list-title-remove"><i class="fa fa-remove"></i></button>
|
||||||
|
<button class="todo-task-list-title-check"><i class="fa fa-check"></i></button>
|
||||||
|
</div>
|
||||||
<div class="add-task-bar">
|
<div class="add-task-bar">
|
||||||
<input type="text" class="add-task" placeholder="{{t 'New task'}}" />
|
<input type="text" class="add-task" placeholder="{{t 'New task'}}" />
|
||||||
<input type="text" class="add-desc" placeholder="{{t 'Description'}}" />
|
<input type="text" class="add-desc" placeholder="{{t 'Description'}}" />
|
||||||
|
|
|
@ -547,7 +547,7 @@ def validate_poll_data(poll_data: object, is_widget_author: bool) -> None:
|
||||||
raise ValidationError(f"Unknown type for poll data: {poll_data['type']}")
|
raise ValidationError(f"Unknown type for poll data: {poll_data['type']}")
|
||||||
|
|
||||||
|
|
||||||
def validate_todo_data(todo_data: object) -> None:
|
def validate_todo_data(todo_data: object, is_widget_author: bool) -> None:
|
||||||
check_dict([("type", check_string)])("todo data", todo_data)
|
check_dict([("type", check_string)])("todo data", todo_data)
|
||||||
|
|
||||||
assert isinstance(todo_data, dict)
|
assert isinstance(todo_data, dict)
|
||||||
|
@ -575,6 +575,19 @@ def validate_todo_data(todo_data: object) -> None:
|
||||||
checker("todo data", todo_data)
|
checker("todo data", todo_data)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if todo_data["type"] == "new_task_list_title":
|
||||||
|
if not is_widget_author:
|
||||||
|
raise ValidationError("You can't edit the task list title unless you are the author.")
|
||||||
|
|
||||||
|
checker = check_dict_only(
|
||||||
|
[
|
||||||
|
("type", check_string),
|
||||||
|
("title", check_string),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
checker("todo data", todo_data)
|
||||||
|
return
|
||||||
|
|
||||||
raise ValidationError(f"Unknown type for todo data: {todo_data['type']}")
|
raise ValidationError(f"Unknown type for todo data: {todo_data['type']}")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,56 @@ def get_widget_data(content: str) -> Tuple[Optional[str], Optional[str]]:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_poll_extra_data(content: str) -> Any:
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
def parse_todo_extra_data(content: str) -> Any:
|
||||||
|
# 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()
|
||||||
|
tasks = []
|
||||||
|
for line in lines:
|
||||||
|
# If someone is using the list syntax, we remove it
|
||||||
|
# before adding a task.
|
||||||
|
task_data = re.sub(r"(\s*[-*]?\s*)", "", line.strip(), count=1)
|
||||||
|
if len(task_data) > 0:
|
||||||
|
# a task and its description (optional) are separated
|
||||||
|
# by the (first) `:` character
|
||||||
|
task_data_array = task_data.split(":", 1)
|
||||||
|
tasks.append(
|
||||||
|
{
|
||||||
|
"task": task_data_array[0].strip(),
|
||||||
|
"desc": task_data_array[1].strip() if len(task_data_array) > 1 else "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
extra_data = {
|
||||||
|
"task_list_title": task_list_title,
|
||||||
|
"tasks": tasks,
|
||||||
|
}
|
||||||
|
return extra_data
|
||||||
|
|
||||||
|
|
||||||
def get_extra_data_from_widget_type(content: str, widget_type: Optional[str]) -> Any:
|
def get_extra_data_from_widget_type(content: str, widget_type: Optional[str]) -> Any:
|
||||||
if widget_type == "poll":
|
if widget_type == "poll":
|
||||||
# This is used to extract the question from the poll command.
|
# This is used to extract the question from the poll command.
|
||||||
|
@ -41,7 +91,15 @@ def get_extra_data_from_widget_type(content: str, widget_type: Optional[str]) ->
|
||||||
"options": options,
|
"options": options,
|
||||||
}
|
}
|
||||||
return extra_data
|
return extra_data
|
||||||
return None
|
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
|
||||||
|
|
||||||
|
|
||||||
def do_widget_post_save_actions(send_request: SendMessageRequest) -> None:
|
def do_widget_post_save_actions(send_request: SendMessageRequest) -> None:
|
||||||
|
|
|
@ -98,10 +98,14 @@ class WidgetContentTestCase(ZulipTestCase):
|
||||||
self.assertEqual(get_widget_data(content=message), (None, None))
|
self.assertEqual(get_widget_data(content=message), (None, None))
|
||||||
|
|
||||||
# Add positive checks for context
|
# Add positive checks for context
|
||||||
self.assertEqual(get_widget_data(content="/todo"), ("todo", None))
|
self.assertEqual(get_widget_data(content="/todo"), ("todo", {"task_list_title": ""}))
|
||||||
self.assertEqual(get_widget_data(content="/todo ignore"), ("todo", None))
|
self.assertEqual(
|
||||||
|
get_widget_data(content="/todo Title"), ("todo", {"task_list_title": "Title"})
|
||||||
|
)
|
||||||
# Test tokenization on newline character
|
# Test tokenization on newline character
|
||||||
self.assertEqual(get_widget_data(content="/todo\nignore"), ("todo", None))
|
self.assertEqual(
|
||||||
|
get_widget_data(content="/todo\nignore"), ("todo", {"task_list_title": ""})
|
||||||
|
)
|
||||||
|
|
||||||
def test_explicit_widget_content(self) -> None:
|
def test_explicit_widget_content(self) -> None:
|
||||||
# Users can send widget_content directly on messages
|
# Users can send widget_content directly on messages
|
||||||
|
@ -167,7 +171,24 @@ class WidgetContentTestCase(ZulipTestCase):
|
||||||
|
|
||||||
expected_submessage_content = dict(
|
expected_submessage_content = dict(
|
||||||
widget_type="todo",
|
widget_type="todo",
|
||||||
extra_data=None,
|
extra_data={"task_list_title": ""},
|
||||||
|
)
|
||||||
|
|
||||||
|
submessage = SubMessage.objects.get(message_id=message.id)
|
||||||
|
self.assertEqual(submessage.msg_type, "widget")
|
||||||
|
self.assertEqual(orjson.loads(submessage.content), expected_submessage_content)
|
||||||
|
|
||||||
|
content = "/todo Example Task List Title"
|
||||||
|
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={"task_list_title": "Example Task List Title"},
|
||||||
)
|
)
|
||||||
|
|
||||||
submessage = SubMessage.objects.get(message_id=message.id)
|
submessage = SubMessage.objects.get(message_id=message.id)
|
||||||
|
@ -226,6 +247,55 @@ class WidgetContentTestCase(ZulipTestCase):
|
||||||
self.assertEqual(submessage.msg_type, "widget")
|
self.assertEqual(submessage.msg_type, "widget")
|
||||||
self.assertEqual(orjson.loads(submessage.content), expected_submessage_content)
|
self.assertEqual(orjson.loads(submessage.content), expected_submessage_content)
|
||||||
|
|
||||||
|
def test_todo_command_extra_data(self) -> None:
|
||||||
|
sender = self.example_user("cordelia")
|
||||||
|
stream_name = "Verona"
|
||||||
|
# We test for leading spaces.
|
||||||
|
content = "/todo School Work"
|
||||||
|
|
||||||
|
payload = dict(
|
||||||
|
type="stream",
|
||||||
|
to=orjson.dumps(stream_name).decode(),
|
||||||
|
topic="whatever",
|
||||||
|
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="School Work",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
submessage = SubMessage.objects.get(message_id=message.id)
|
||||||
|
self.assertEqual(submessage.msg_type, "widget")
|
||||||
|
self.assertEqual(orjson.loads(submessage.content), expected_submessage_content)
|
||||||
|
|
||||||
|
# Now don't supply a task list title.
|
||||||
|
|
||||||
|
content = "/todo"
|
||||||
|
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="",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
def test_poll_permissions(self) -> None:
|
def test_poll_permissions(self) -> None:
|
||||||
cordelia = self.example_user("cordelia")
|
cordelia = self.example_user("cordelia")
|
||||||
hamlet = self.example_user("hamlet")
|
hamlet = self.example_user("hamlet")
|
||||||
|
@ -255,6 +325,37 @@ class WidgetContentTestCase(ZulipTestCase):
|
||||||
result = post(hamlet, dict(type="question", question="Tabs or spaces?"))
|
result = post(hamlet, dict(type="question", question="Tabs or spaces?"))
|
||||||
self.assert_json_error(result, "You can't edit a question unless you are the author.")
|
self.assert_json_error(result, "You can't edit a question unless you are the author.")
|
||||||
|
|
||||||
|
def test_todo_permissions(self) -> None:
|
||||||
|
cordelia = self.example_user("cordelia")
|
||||||
|
hamlet = self.example_user("hamlet")
|
||||||
|
stream_name = "Verona"
|
||||||
|
content = "/todo School Work"
|
||||||
|
|
||||||
|
payload = dict(
|
||||||
|
type="stream",
|
||||||
|
to=orjson.dumps(stream_name).decode(),
|
||||||
|
topic="whatever",
|
||||||
|
content=content,
|
||||||
|
)
|
||||||
|
result = self.api_post(cordelia, "/api/v1/messages", payload)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
message = self.get_last_message()
|
||||||
|
|
||||||
|
def post(sender: UserProfile, data: Dict[str, object]) -> "TestHttpResponse":
|
||||||
|
payload = dict(
|
||||||
|
message_id=message.id, msg_type="widget", content=orjson.dumps(data).decode()
|
||||||
|
)
|
||||||
|
return self.api_post(sender, "/api/v1/submessage", payload)
|
||||||
|
|
||||||
|
result = post(cordelia, dict(type="new_task_list_title", title="School Work"))
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
result = post(hamlet, dict(type="new_task_list_title", title="School Work"))
|
||||||
|
self.assert_json_error(
|
||||||
|
result, "You can't edit the task list title unless you are the author."
|
||||||
|
)
|
||||||
|
|
||||||
def test_poll_type_validation(self) -> None:
|
def test_poll_type_validation(self) -> None:
|
||||||
sender = self.example_user("cordelia")
|
sender = self.example_user("cordelia")
|
||||||
stream_name = "Verona"
|
stream_name = "Verona"
|
||||||
|
|
|
@ -49,7 +49,7 @@ def process_submessage(
|
||||||
|
|
||||||
if widget_type == "todo":
|
if widget_type == "todo":
|
||||||
try:
|
try:
|
||||||
validate_todo_data(todo_data=widget_data)
|
validate_todo_data(todo_data=widget_data, is_widget_author=is_widget_author)
|
||||||
except ValidationError as error:
|
except ValidationError as error:
|
||||||
raise JsonableError(error.message)
|
raise JsonableError(error.message)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue