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

model: Fetch realm emoji using events #827

Merged
merged 4 commits into from
May 5, 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
51 changes: 45 additions & 6 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,44 @@ def streams_fixture():


@pytest.fixture
def custom_emojis():
def realm_emojis():
# Omitting source_url, author_id (server version 3.0),
# author (server version < 3.0) since they are not used.
return {
"1": {
"deactivated": True,
"id": "1",
"name": "green_tick",
},
"202020": {
"deactivated": False,
"id": "202020",
"name": "joker",
},
"2": {
"deactivated": True,
"id": "2",
"name": "joy_cat",
},
"3": {
"deactivated": False,
"id": "3",
"name": "singing",
},
"4": {
"deactivated": False,
"id": "4",
"name": "zulip",
}
}


@pytest.fixture
def realm_emojis_data():
return OrderedDict([
('urwid', {'code': '100', 'type': 'realm_emoji'}),
('dancing', {'code': '3', 'type': 'realm_emoji'}),
('snape', {'code': '20', 'type': 'realm_emoji'}),
('joker', {'code': '202020', 'type': 'realm_emoji'}),
('zulip', {'code': '12345', 'type': 'realm_emoji'}),
('singing', {'code': '3', 'type': 'realm_emoji'}),
neiljp marked this conversation as resolved.
Show resolved Hide resolved
('zulip', {'code': '4', 'type': 'realm_emoji'}),
])


Expand All @@ -208,6 +239,13 @@ def unicode_emojis():
])


@pytest.fixture
def zulip_emoji():
return OrderedDict([
('zulip', {'code': 'zulip', 'type': 'zulip_extra_emoji'})
])


stream_msg_template = {
'id': 537286,
'sender_full_name': 'Foo Foo',
Expand Down Expand Up @@ -388,7 +426,7 @@ def topics():


@pytest.fixture
def initial_data(logged_on_user, users_fixture, streams_fixture):
def initial_data(logged_on_user, users_fixture, streams_fixture, realm_emojis):
"""
Response from /register API request.
"""
Expand Down Expand Up @@ -540,6 +578,7 @@ def initial_data(logged_on_user, users_fixture, streams_fixture):
}
},
'twenty_four_hour_time': True,
'realm_emoji': realm_emojis,
'last_event_id': -1,
'muted_topics': [],
'realm_user_groups': [],
Expand Down
125 changes: 82 additions & 43 deletions tests/model/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def mock_external_classes(self, mocker: Any) -> None:

@pytest.fixture
def model(self, mocker, initial_data, user_profile,
unicode_emojis, custom_emojis):
unicode_emojis):
mocker.patch('zulipterminal.model.Model.get_messages',
return_value='')
self.client.register.return_value = initial_data
Expand All @@ -47,13 +47,11 @@ def model(self, mocker, initial_data, user_profile,
self.client.get_profile.return_value = user_profile
mocker.patch('zulipterminal.model.unicode_emojis',
EMOJI_DATA=unicode_emojis)
mocker.patch('zulipterminal.model.Model.fetch_custom_emojis',
return_value=custom_emojis)
model = Model(self.controller)
return model

def test_init(self, model, initial_data, user_profile,
unicode_emojis, custom_emojis, stream_dict):
unicode_emojis, realm_emojis_data, zulip_emoji, stream_dict):
assert hasattr(model, 'controller')
assert hasattr(model, 'client')
assert model.narrow == []
Expand All @@ -79,13 +77,12 @@ def test_init(self, model, initial_data, user_profile,
assert model.users == []
self.classify_unread_counts.assert_called_once_with(model)
assert model.unread_counts == []
zulip_emoji = {
'zulip': {'code': 'zulip', 'type': 'zulip_extra_emoji'}
}
assert model.active_emoji_data == OrderedDict(sorted(
{**unicode_emojis, **custom_emojis, **zulip_emoji}.items(),
{**unicode_emojis, **realm_emojis_data, **zulip_emoji}.items(),
key=lambda e: e[0]
))
# Deactivated emoji is removed from active emojis set
assert 'green_tick' not in model.active_emoji_data
# Custom emoji replaces unicode emoji with same name.
assert model.active_emoji_data['joker']['type'] == 'realm_emoji'
# zulip_extra_emoji replaces all other emoji types for 'zulip' emoji.
Expand Down Expand Up @@ -177,6 +174,7 @@ def test_register_initial_desired_events(self, mocker, initial_data):
'typing',
'update_message_flags',
'update_display_settings',
'realm_emoji',
]
fetch_event_types = [
'realm',
Expand All @@ -188,6 +186,7 @@ def test_register_initial_desired_events(self, mocker, initial_data):
'realm_user',
'realm_user_groups',
'update_display_settings',
'realm_emoji',
'zulip_version',
]
model.client.register.assert_called_once_with(
Expand Down Expand Up @@ -2112,44 +2111,84 @@ def test_user_name_from_id_invalid(self, model, user_id):
with pytest.raises(RuntimeError, match='Invalid user ID.'):
model.user_name_from_id(user_id)

@pytest.mark.parametrize('response', [
{
# Omitting source_url, author_id (server version 3.0),
# author (server version < 3.0) from response since they
# are not used.
'emoji': {
'100': {
'deactivated': False,
neiljp marked this conversation as resolved.
Show resolved Hide resolved
'id': '100',
'name': 'urwid',
},
'20': {
'deactivated': False,
'id': '20',
'name': 'snape',
},
'3': {
'deactivated': False,
'id': '3',
'name': 'dancing',
},
'81': { # Not included in custom_emojis because deactivated.
'deactivated': True,
'id': '81',
'name': 'green_tick',
},
},
'msg': '',
'result': 'success'
def test_generate_all_emoji_data(self, mocker, model, zulip_emoji,
unicode_emojis, realm_emojis_data,
realm_emojis):
all_emoji_data = model.generate_all_emoji_data(realm_emojis)

assert all_emoji_data == OrderedDict(sorted(
{**unicode_emojis, **realm_emojis_data, **zulip_emoji}.items(),
key=lambda e: e[0]
))
# custom emoji added to active_emoji_data
assert all_emoji_data['singing']['type'] == 'realm_emoji'
# Deactivated custom emoji is removed from active emojis set
assert 'green_tick' not in all_emoji_data
# deactivated custom emoji which replaced unicode emoji with same name
assert all_emoji_data['joy_cat']['type'] == 'unicode_emoji'
# Custom emoji replaces unicode emoji with same name.
assert all_emoji_data['joker']['type'] == 'realm_emoji'
# zulip_extra_emoji replaces all other emoji types for 'zulip' emoji.
assert all_emoji_data['zulip']['type'] == 'zulip_extra_emoji'

@pytest.mark.parametrize(['to_vary_in_realm_emoji',
'emoji_should_be_active'], [
({
"deactivated": False,
"id": "20",
"name": "joy_cat",
}, True),
({
"deactivated": True,
"id": "202020",
"name": "joker",
}, True),
({
"deactivated": False,
"id": "22",
"name": "zulip",
}, True),
({
"deactivated": True,
"id": "4",
"name": "zulip",
}, True),
({
"deactivated": False,
"id": "23",
"name": "new_emoji",
}, True),
({
"deactivated": True,
"id": "3",
"name": "singing",
}, False),
],
ids=[
'realm_emoji_with_same_name_as_unicode_emoji_added',
'realm_emoji_with_same_name_as_unicode_emoji_removed',
'realm_emoji_with_name_as_zulip_added',
'realm_emoji_with_name_as_zulip_removed',
'realm_emoji_added',
'realm_emoji_removed',
]
)
def test__handle_update_emoji_event(self, mocker, model, realm_emojis,
emoji_should_be_active,
to_vary_in_realm_emoji):
emoji_name = to_vary_in_realm_emoji['name']
realm_emojis[to_vary_in_realm_emoji['id']] = to_vary_in_realm_emoji
event = {
'type': 'realm_emoji',
'realm_emoji': realm_emojis
}
])
def test_fetch_custom_emojis(self, mocker, model, custom_emojis,
neiljp marked this conversation as resolved.
Show resolved Hide resolved
response):
self.client.get_realm_emoji.return_value = response

fetched_custom_emojis = model.fetch_custom_emojis()
model._handle_update_emoji_event(event)

assert fetched_custom_emojis == custom_emojis
if emoji_should_be_active:
assert emoji_name in model.active_emoji_data
else:
assert emoji_name not in model.active_emoji_data

# Use LoopEnder with raising_event to cause the event loop to end without
# processing the event
Expand Down
15 changes: 15 additions & 0 deletions zulipterminal/api_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,20 @@ class UpdateDisplaySettings(TypedDict):
setting: bool


class RealmEmojiData(TypedDict):
id: str
name: str
source_url: str
deactivated: bool
# Previous versions had an author object with an id field.
author_id: int # NOTE: new in Zulip 3.0 / ZFL 7.


class UpdateRealmEmojiEvent(TypedDict):
type: Literal['realm_emoji']
realm_emoji: Dict[str, RealmEmojiData]


Event = Union[
MessageEvent,
UpdateMessageEvent,
Expand All @@ -156,4 +170,5 @@ class UpdateDisplaySettings(TypedDict):
TypingEvent,
UpdateMessageFlagsEvent,
UpdateDisplaySettings,
UpdateRealmEmojiEvent,
]
55 changes: 36 additions & 19 deletions zulipterminal/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
EditPropagateMode,
Event,
PrivateComposition,
RealmEmojiData,
StreamComposition,
Subscription,
)
Expand Down Expand Up @@ -107,6 +108,7 @@ def __init__(self, controller: Any) -> None:
'realm_user', # Enables cross_realm_bots
'realm_user_groups',
'update_display_settings',
'realm_emoji',
# zulip_version and zulip_feature_level are always returned in
# POST /register from Feature level 3.
'zulip_version',
Expand All @@ -124,6 +126,7 @@ def __init__(self, controller: Any) -> None:
self._handle_update_message_flags_event),
('update_display_settings',
self._handle_update_display_settings_event),
('realm_emoji', self._handle_update_emoji_event),
])
)

Expand Down Expand Up @@ -165,21 +168,12 @@ def __init__(self, controller: Any) -> None:
self.unread_counts = classify_unread_counts(self)

self._draft: Optional[Composition] = None
unicode_emoji_data = unicode_emojis.EMOJI_DATA
for name, data in unicode_emoji_data.items():
data['type'] = 'unicode_emoji'
typed_unicode_emoji_data = cast(NamedEmojiData, unicode_emoji_data)
custom_emoji_data = self.fetch_custom_emojis()
zulip_extra_emoji: NamedEmojiData = {
'zulip': {'code': 'zulip', 'type': 'zulip_extra_emoji'}
}
all_emoji_data = {**typed_unicode_emoji_data,
**custom_emoji_data,
**zulip_extra_emoji}.items()
self.active_emoji_data = OrderedDict(sorted(all_emoji_data,
key=lambda e: e[0]))

self._store_content_length_restrictions()

self.active_emoji_data = self.generate_all_emoji_data(
self.initial_data['realm_emoji'])

self.twenty_four_hr_format = self.initial_data['twenty_four_hour_time']
self.new_user_input = True
self._start_presence_updates()
Expand Down Expand Up @@ -487,18 +481,30 @@ def update_stream_message(self, topic: str, message_id: int,

return response['result'] == 'success'

def fetch_custom_emojis(self) -> NamedEmojiData:
response = self.client.get_realm_emoji()
custom_emojis: NamedEmojiData = {
def generate_all_emoji_data(self,
custom_emoji: Dict[str, RealmEmojiData]
) -> NamedEmojiData:
unicode_emoji_data = unicode_emojis.EMOJI_DATA
for name, data in unicode_emoji_data.items():
data['type'] = 'unicode_emoji'
typed_unicode_emoji_data = cast(NamedEmojiData, unicode_emoji_data)
custom_emoji_data: NamedEmojiData = {
emoji['name']: {
'code': emoji_code,
'type': 'realm_emoji',
}
for emoji_code, emoji in response['emoji'].items()
for emoji_code, emoji in custom_emoji.items()
if not emoji['deactivated']
}
display_error_if_present(response, self.controller)
return custom_emojis
zulip_extra_emoji: NamedEmojiData = {
'zulip': {'code': 'zulip', 'type': 'zulip_extra_emoji'}
}
all_emoji_data = {**typed_unicode_emoji_data,
**custom_emoji_data,
**zulip_extra_emoji}.items()
active_emoji_data = OrderedDict(sorted(all_emoji_data,
key=lambda e: e[0]))
return active_emoji_data

def get_messages(self, *,
num_after: int, num_before: int,
Expand Down Expand Up @@ -1268,6 +1274,17 @@ def formatted_local_time(
)
return local_time.strftime(format_codes)

def _handle_update_emoji_event(self, event: Event) -> None:
"""
Handle update of emoji
"""
# Here, the event contains information of all realm emojis added
# by the users in the organisation along with a boolean value
# representing the active state of each emoji.
assert event['type'] == "realm_emoji"
self.active_emoji_data = self.generate_all_emoji_data(
event['realm_emoji'])
neiljp marked this conversation as resolved.
Show resolved Hide resolved

def _update_rendered_view(self, msg_id: int) -> None:
"""
Helper method called by various _handle_* methods
Expand Down