Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

messages: WIP poll widget #1477

Closed
wants to merge 4 commits into from
Closed

messages: WIP poll widget #1477

wants to merge 4 commits into from

Conversation

rsashank
Copy link
Member

@rsashank rsashank commented Mar 28, 2024

What does this PR do, and why?

Implementation for the polls widget WIP

Previous:
image

Current PR:
image

Outstanding aspect(s)

  • [ ]

External discussion & connections

How did you test this?

  • Manually - Behavioral changes
  • Manually - Visual changes
  • Adapting existing automated tests
  • Adding automated tests for new behavior (or missing tests)
  • Existing automated tests should already cover this (only a refactor of tested code)

Self-review checklist for each commit

  • It is a minimal coherent idea
  • It has a commit summary following the documented style (title & body)
  • It has a commit summary describing the motivation and reasoning for the change
  • It individually passes linting and tests
  • It contains test additions for any new behavior
  • It flows clearly from a previous branch commit, and/or prepares for the next commit

Visual changes

@zulipbot zulipbot added the size: M [Automatic label added by zulipbot] label Mar 28, 2024
@rsashank rsashank force-pushed the polls branch 2 times, most recently from e91468a to cb6bfc8 Compare March 28, 2024 17:09
@neiljp neiljp added area: message rendering PR awaiting update PR has been reviewed & is awaiting update or response to reviewer feedback labels Apr 6, 2024
@zulipbot zulipbot added size: XL [Automatic label added by zulipbot] and removed size: M [Automatic label added by zulipbot] labels Jun 3, 2024
@rsashank rsashank force-pushed the polls branch 5 times, most recently from 43631a4 to 7bd95e0 Compare June 4, 2024 14:27
@rsashank rsashank added feedback wanted and removed PR awaiting update PR has been reviewed & is awaiting update or response to reviewer feedback labels Jun 6, 2024
Copy link
Collaborator

@neiljp neiljp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rsashank This looks to work well as a first version in practice 👍

I left some detailed feedback, but essentially separating the model and messages code will keep things more organized and easier to test, as well as make it easier to move forward from this point. The sooner we can handle submessage events, the more dynamic and useful polls will be.

Comment on lines 733 to 745
# If message contains submessages (like polls, todo), process them
# and update the message content.
if self.message["submessages"]:
try:
first_submessage_content = json.loads(
self.message["submessages"][0]["content"]
)
except (json.JSONDecodeError, TypeError):
first_submessage_content = {}

if (
"widget_type" in first_submessage_content
and first_submessage_content["widget_type"] == "poll"
):
question, votes_for_option = self.process_poll_data(
self.message["submessages"]
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of this code (733-749) is related to rendering/UI, which is primarily what the message-box code is concerned with.

Specifically, this code is only concerned with handling the (sub)messages, including in .process_poll_data, so it could easily belong somewhere else - it doesn't touch this object itself, and the new method is a class method (and doesn't use cls).

As I noted at the end of March in the channel, it'd be cleaner to separate submessage processing into the model, and have that return the equivalent kind of data as from process_poll_data, or possibly in the form of a new object. Did you understand my comment there?

The lines here could basically become something like

submessage_content = model.processed_submessages_for_message(message_id)
if submessage_content is not None:
    if submessage_content.type == "poll":
        ...

We also may want the processing in the model since:

  • the model will also need to handle updating of submessages (eg. new votes coming in, questions)
  • the model is where we have historically wrapped sending requests to the server (eg. active user voting in other polls)
  • the processed data could be cached in the model, if necessary (only updated on events, not recalculated)

It may be reasonable to place some of the processing code into another file instead of the model, like we do with rendering tables, but let's see what we end up with first.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extending Neil's comment, since this function is to be added in the model file, the code patch for process_poll_data and its test can be shifted to a separate commit which comes before this commit.

Comment on lines 751 to 760
if question:
self.message[
"content"
] = f"<strong>Poll Question: {question}</strong>\n"
else:
self.message["content"] = "<strong>Add Poll Question</strong>\n"
for option, voters in votes_for_option.items():
voter_count = len(voters)
count_text = f"<strong>[{voter_count:^3}]</strong>"
option_text = f"{option}"
self.message["content"] += f"{count_text} {option_text}\n"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This formatting looks fine for now, just for testing 👍

For various future iterations, possibly in order of importance:

  • Each poll appears to have a blank line trailing, which is likely since you're adding a "...\n" each time, rather than "\n".join(list_of_strings); check the difference if you're not familiar with that :)
  • It may be enough to prefix the title/question with only Poll: ?
  • Keeping the Poll: styling strong and distinct from a not-strong title/question might make it clearer that it's a poll? (and confirm the original text)
  • Until we support updates, having 'Add Poll Question' looks somewhat odd when it's not possible, so maybe have (no title) or skip the line?
  • Similarly it may be worth showing some indicator that there are no options if that's the case, eg. (no options available)
  • Counts of eg. 0 and 11 don't line up correctly, or look oddly spaced; if we want a space either side of all numbers, we maybe want the width of 3 to be dynamic instead
  • We could treat voter names, etc. like we do with limiting reacting user names beyond a certain number, at least in the message itself; that said, I suspect a poll or widget popup will be likely, so that information can be shown there, as you said.

As per my note in March, while we overwrite the content for /me, I'd prefer we avoid that pattern, since that modifies the original message - if only slightly in that case. It would be reasonable to refactor before this change to pass eg. a new content_to_render to transform_content, which might initially be the direct message content or the f-string from /me. Then you can add a further alternative in this work, which would set that string to the one you build above.

Comment on lines 1483 to 1487
ids=[
"poll_with_votes",
"poll_without_question",
"poll_without_options",
],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please have ids inline - there are only 3 cases, but they're each quite long.

"message_id": 1813721,
"sender_id": 27294,
"msg_type": "widget",
"content": '{"widget_type": "poll", "extra_data": {"question": "Can you view polls on Zulip Terminal?", "options": ["Yes", "No"]}}', # noqa: E501
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to ask why you didn't just let black handle this, rather than use noqa, but this is related to the content being json. I agree that noqa could be easier for pasting test cases in reliably - pasting the line, just adding noqa - but it's still kinda annoying to read.

I'd suggest we could split the string and rely on automatic string concatenation, which could be easier to read; for example, that allows this:

assert (
   "string1 "
   "string2"
) == "string1 string2"

Alternatively, particularly as your cases become more complex, you may wish to use a factory fixture to generate the submessages (or even a full message with submessages). One reason for this is that the message_id should be identical for submessages, I'd expect, and msg_type is likely widget for all polls - and widget_type is poll, etc. That all needs confirming, but could allow you to have something in a test like the following which would generate data from certain templates:

submessages = poll_factory(
    {"question": "what?", "options": ["cheese"], "sender_id": 5},
    {"vote", ..., "sender_id": 6},
    message_id=1813721  # or just rely on a default
)

@neiljp neiljp added area: widgets PR awaiting update PR has been reviewed & is awaiting update or response to reviewer feedback labels Jun 12, 2024
Copy link
Collaborator

@mounilKshah mounilKshah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rsashank for taking this up! It will be great to have this feature on ZT
I tested this on my system and it worked as per expectations. I didn't face any glitches.
Apart from the test function that you have added for process_poll_data, I think it will be a good idea from integration test point of view to add a test case for transform_content which tests for poll data. I'll take this up in the stream.

Comment on lines 733 to 745
# If message contains submessages (like polls, todo), process them
# and update the message content.
if self.message["submessages"]:
try:
first_submessage_content = json.loads(
self.message["submessages"][0]["content"]
)
except (json.JSONDecodeError, TypeError):
first_submessage_content = {}

if (
"widget_type" in first_submessage_content
and first_submessage_content["widget_type"] == "poll"
):
question, votes_for_option = self.process_poll_data(
self.message["submessages"]
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extending Neil's comment, since this function is to be added in the model file, the code patch for process_poll_data and its test can be shifted to a separate commit which comes before this commit.

@@ -729,6 +730,36 @@ def main_view(self) -> List[Any]:
"/me", f"<strong>{self.message['sender_full_name']}</strong>", 1
)

# If message contains submessages (like polls, todo), process them
# and update the message content.
if self.message["submessages"]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this point.
A lot of test cases failed due to KeyError at this line. You can try using get() instead of using key to fetch the data from the dictionary

@neiljp
Copy link
Collaborator

neiljp commented Jul 30, 2024

@rsashank To ensure my feedback is visible, following up from some points elsewhere, my suggestions at this point would be to:

  • treat the poll data processing separately, even in a new file; this will make it easier to read and think about separately, and think of tests to cover it well
  • render polls very simply for now - polls don't support markup (Support some Markdown in polls zulip#21917); this means we can go direct to urwid markup for them, use a conditional vs the normal processing of Zulip HTML into urwid (this also supports not overwriting message data, as per my comments above)
  • separate at least the code for the 'is a poll'/'get the poll data' and 'render the poll'; the rendering part only cares about taking the processed poll data and rendering it (not the raw data), the model knows best about the other parts
  • the parts for 'is a poll' and 'get the poll data' (processed) could be separate or a single model method; the different approaches just affect how you call them and whether data is implicitly processed.

Added find_widget_type to identify the type of widget present in the message
(e.g., polls, todo, etc.).

Added test.
Added process_poll_widget function to process submessages
containing a poll widget. Returns the question of the poll
along with options and their voters in a dict.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: message rendering area: widgets feedback wanted PR awaiting update PR has been reviewed & is awaiting update or response to reviewer feedback size: XL [Automatic label added by zulipbot]
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants