Skip to content

boxes: Use user_ids in autocomplete for user mentions. #928

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

Merged
merged 2 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ def users_fixture(logged_on_user):
"is_admin": False,
}
)
# Also add two users with the same name.
for i in range(1, 3):
users.append(
{
"user_id": 12 + i,
"full_name": "Human Duplicate",
"email": f"personduplicate{i}@example.com",
"avatar_url": None,
"is_active": True,
"bot_type": None,
"is_bot": False,
"is_admin": False,
}
)
return users


Expand Down Expand Up @@ -811,6 +825,18 @@ def user_dict(logged_on_user):
"user_id": 12,
"status": "inactive",
},
"[email protected]": {
"full_name": "Human Duplicate",
"email": "[email protected]",
"user_id": 13,
"status": "inactive",
},
"[email protected]": {
"full_name": "Human Duplicate",
"email": "[email protected]",
"user_id": 14,
"status": "inactive",
},
"[email protected]": {
"email": "[email protected]",
"full_name": "Email Gateway",
Expand Down Expand Up @@ -870,6 +896,18 @@ def user_list(logged_on_user):
"user_id": 12,
"status": "inactive",
},
{
"full_name": "Human Duplicate",
"email": "[email protected]",
"user_id": 13,
"status": "inactive",
},
{
"full_name": "Human Duplicate",
"email": "[email protected]",
"user_id": 14,
"status": "inactive",
},
{
"email": "[email protected]",
"full_name": "Notification Bot",
Expand Down
185 changes: 161 additions & 24 deletions tests/ui_tools/test_boxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,21 +101,83 @@ def test_generic_autocomplete_no_prefix(self, mocker, write_box, text, state):
"Human Myself",
"Human 1",
"Human 2",
"Human Duplicate",
"Human Duplicate",
"Group 1",
"Group 2",
"Group 3",
"Group 4",
],
),
("@*", 0, ["Group 1", "Group 2", "Group 3", "Group 4"]),
("@**", 0, ["Human Myself", "Human 1", "Human 2"]),
(
"@**",
0,
[
"Human Myself",
"Human 1",
"Human 2",
"Human Duplicate",
"Human Duplicate",
],
),
# mentions
("@Human", 0, ["Human Myself", "Human 1", "Human 2"]),
("@**Human", 0, ["Human Myself", "Human 1", "Human 2"]),
("@_Human", 0, ["Human Myself", "Human 1", "Human 2"]),
(
"@Human",
0,
[
"Human Myself",
"Human 1",
"Human 2",
"Human Duplicate",
"Human Duplicate",
],
),
(
"@**Human",
0,
[
"Human Myself",
"Human 1",
"Human 2",
"Human Duplicate",
"Human Duplicate",
],
),
(
"@_Human",
0,
[
"Human Myself",
"Human 1",
"Human 2",
"Human Duplicate",
"Human Duplicate",
],
),
("@_*Human", None, []), # NOTE: Optional single star fails
("@_**Human", 0, ["Human Myself", "Human 1", "Human 2"]),
("@Human", None, ["Human Myself", "Human 1", "Human 2"]),
(
"@_**Human",
0,
[
"Human Myself",
"Human 1",
"Human 2",
"Human Duplicate",
"Human Duplicate",
],
),
(
"@Human",
None,
[
"Human Myself",
"Human 1",
"Human 2",
"Human Duplicate",
"Human Duplicate",
],
),
("@NoMatch", None, []),
# streams
(
Expand Down Expand Up @@ -159,13 +221,19 @@ def test_generic_autocomplete_set_footer(
("@Human", 0, "@**Human Myself**"),
("@Human", 1, "@**Human 1**"),
("@Human", 2, "@**Human 2**"),
("@Human", -1, "@**Human 2**"),
("@Human", -2, "@**Human 1**"),
("@Human", -3, "@**Human Myself**"),
("@Human", -4, None),
("@Human", 3, "@**Human Duplicate|13**"),
("@Human", 4, "@**Human Duplicate|14**"),
("@Human", -1, "@**Human Duplicate|14**"),
("@Human", -2, "@**Human Duplicate|13**"),
("@Human", -3, "@**Human 2**"),
("@Human", -4, "@**Human 1**"),
("@Human", -5, "@**Human Myself**"),
("@Human", -6, None),
("@_Human", 0, "@_**Human Myself**"),
("@_Human", 1, "@_**Human 1**"),
("@_Human", 2, "@_**Human 2**"),
("@_Human", 3, "@_**Human Duplicate|13**"),
("@_Human", 4, "@_**Human Duplicate|14**"),
("@H", 1, "@**Human 1**"),
("@Hu", 1, "@**Human 1**"),
("@Hum", 1, "@**Human 1**"),
Expand All @@ -192,18 +260,22 @@ def test_generic_autocomplete_set_footer(
("@", 0, "@**Human Myself**"),
("@", 1, "@**Human 1**"),
("@", 2, "@**Human 2**"),
("@", 3, "@*Group 1*"),
("@", 4, "@*Group 2*"),
("@", 5, "@*Group 3*"),
("@", 6, "@*Group 4*"),
("@", 7, None), # Reached last match
("@", 8, None), # Beyond end
("@", 3, "@**Human Duplicate|13**"),
("@", 4, "@**Human Duplicate|14**"),
("@", 5, "@*Group 1*"),
("@", 6, "@*Group 2*"),
("@", 7, "@*Group 3*"),
("@", 8, "@*Group 4*"),
("@", 9, None), # Reached last match
("@", 10, None), # Beyond end
# Expected sequence of autocompletes from '@**' (no groups)
("@**", 0, "@**Human Myself**"),
("@**", 1, "@**Human 1**"),
("@**", 2, "@**Human 2**"),
("@**", 3, None), # Reached last match
("@**", 4, None), # Beyond end
("@", 3, "@**Human Duplicate|13**"),
("@", 4, "@**Human Duplicate|14**"),
("@**", 5, None), # Reached last match
("@**", 6, None), # Beyond end
# Expected sequence of autocompletes from '@*' (only groups)
("@*", 0, "@*Group 1*"),
("@*", 1, "@*Group 2*"),
Expand All @@ -215,9 +287,11 @@ def test_generic_autocomplete_set_footer(
("@_", 0, "@_**Human Myself**"), # NOTE: No silent group mention
("@_", 1, "@_**Human 1**"),
("@_", 2, "@_**Human 2**"),
("@_", 3, None), # Reached last match
("@_", 4, None), # Beyond end
("@_", -1, "@_**Human 2**"),
("@_", 3, "@_**Human Duplicate|13**"),
("@_", 4, "@_**Human Duplicate|14**"),
("@_", 5, None), # Reached last match
("@_", 6, None), # Beyond end
("@_", -1, "@_**Human Duplicate|14**"),
# Complex autocomplete prefixes.
("(@H", 0, "(@**Human Myself**"),
("(@H", 1, "(@**Human 1**"),
Expand Down Expand Up @@ -266,6 +340,41 @@ def test_generic_autocomplete_mentions_subscribers(
typeahead_string = write_box.generic_autocomplete(text, state)
assert typeahead_string == required_typeahead

@pytest.mark.parametrize(
"text, expected_distinct_prefix",
# Add 3 different lists of tuples, with each tuple containing a combination
# of the text to be autocompleted and the corresponding typeahead prefix to
# be added to the typeahead suggestions. Only the "@" case has to be ignored
# while building the parameters, because it includes group suggestions too.
[("@" + "Human"[: index + 1], "@") for index in range(len("Human"))]
+ [("@**" + "Human"[:index], "@") for index in range(len("Human") + 1)]
+ [("@_" + "Human"[:index], "@_") for index in range(len("Human") + 1)],
)
def test_generic_autocomplete_user_mentions(
self, write_box, mocker, text, expected_distinct_prefix, state=1
):
_process_typeaheads = mocker.patch(BOXES + ".WriteBox._process_typeaheads")

write_box.generic_autocomplete(text, state)

matching_users = [
"Human Myself",
"Human 1",
"Human 2",
"Human Duplicate",
"Human Duplicate",
]
distinct_matching_users = [
expected_distinct_prefix + "**Human Myself**",
expected_distinct_prefix + "**Human 1**",
expected_distinct_prefix + "**Human 2**",
expected_distinct_prefix + "**Human Duplicate|13**",
expected_distinct_prefix + "**Human Duplicate|14**",
]
_process_typeaheads.assert_called_once_with(
distinct_matching_users, state, matching_users
)

@pytest.mark.parametrize(
"text, state, required_typeahead, to_pin",
[
Expand Down Expand Up @@ -383,11 +492,19 @@ def test_generic_autocomplete_emojis(
[
(
"",
["Human Myself", "Human 1", "Human 2"],
[
"Human Myself",
"Human 1",
"Human 2",
"Human Duplicate",
"Human Duplicate",
],
[
"Human Myself <[email protected]>",
"Human 1 <[email protected]>",
"Human 2 <[email protected]>",
"Human Duplicate <[email protected]>",
"Human Duplicate <[email protected]>",
],
),
("My", ["Human Myself"], ["Human Myself <[email protected]>"]),
Expand Down Expand Up @@ -436,14 +553,24 @@ def test__to_box_autocomplete_with_spaces(
[
(
"Welcome Bot <[email protected]>, Human",
["Human Myself", "Human 1", "Human 2"],
[
"Human Myself",
"Human 1",
"Human 2",
"Human Duplicate",
"Human Duplicate",
],
[
"Welcome Bot <[email protected]>, "
"Human Myself <[email protected]>",
"Welcome Bot <[email protected]>, "
"Human 1 <[email protected]>",
"Welcome Bot <[email protected]>, "
"Human 2 <[email protected]>",
"Welcome Bot <[email protected]>, "
"Human Duplicate <[email protected]>",
"Welcome Bot <[email protected]>, "
"Human Duplicate <[email protected]>",
],
),
(
Expand All @@ -457,14 +584,24 @@ def test__to_box_autocomplete_with_spaces(
),
(
"Email Gateway <[email protected]>,Human",
["Human Myself", "Human 1", "Human 2"],
[
"Human Myself",
"Human 1",
"Human 2",
"Human Duplicate",
"Human Duplicate",
],
[
"Email Gateway <[email protected]>, "
"Human Myself <[email protected]>",
"Email Gateway <[email protected]>, "
"Human 1 <[email protected]>",
"Email Gateway <[email protected]>, "
"Human 2 <[email protected]>",
"Email Gateway <[email protected]>, "
"Human Duplicate <[email protected]>",
"Email Gateway <[email protected]>, "
"Human Duplicate <[email protected]>",
],
),
(
Expand Down
19 changes: 17 additions & 2 deletions zulipterminal/ui_tools/boxes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re
import typing
import unicodedata
from collections import OrderedDict, defaultdict
from collections import Counter, OrderedDict, defaultdict
from datetime import date, datetime, timedelta
from sys import platform
from time import sleep, time
Expand Down Expand Up @@ -457,12 +457,27 @@ def autocomplete_users(
)

user_names = [user["full_name"] for user in sorted_matching_users]

# Counter holds a count of each name in the list of users' names in a
# dict-like manner, which is a more efficient approach when compared to
# slicing the original list on each name.
# FIXME: Use a persistent counter rather than generate one on each autocomplete.
user_names_counter = Counter(user_names)

# Append user_id's to users with the same names.
user_names_with_distinct_duplicates = [
f"{user['full_name']}|{user['user_id']}"
if user_names_counter[user["full_name"]] > 1
else user["full_name"]
for user in sorted_matching_users
]

extra_prefix = "{}{}".format(
"*" if prefix_string[-1] != "*" else "",
"*" if prefix_string[-2:] != "**" else "",
)
user_typeahead = format_string(
user_names, prefix_string + extra_prefix + "{}**"
user_names_with_distinct_duplicates, prefix_string + extra_prefix + "{}**"
)

return user_typeahead, user_names
Expand Down