class TimelineListTest(ActiveListTest): def active_index(self): return self.timeline_list.active_index # - Helpers --------------------------------------------------------------- def append_timeline(self): self.timeline_list.append_timeline(Timeline('Timeline')) def assert_visible(self, visible_list): self.assertEqual(self.timeline_list.visible, visible_list) # - Tests ----------------------------------------------------------------- def setUp(self): self.timeline_list = TimelineList() def test_has_timelines_false_if_empty(self): self.failIf(self.timeline_list.has_timelines()) def test_has_timelines_true_otherwise(self): self.append_timeline() self.failUnless(self.timeline_list.has_timelines()) def test_null_index_with_no_timelines(self): self.assert_null_index() def test_active_index_0_when_appending_first_timeline(self): self.append_timeline() self.assertEqual(self.timeline_list.active_index, 0) def test_activate_previous(self): # null index when there are no timelines self.timeline_list.activate_previous() self.assert_null_index() # does not change if its the first self.append_timeline() self.assertEqual(self.timeline_list.active_index, 0) self.timeline_list.activate_previous() self.assertEqual(self.timeline_list.active_index, 0) def test_activate_next(self): # null index when there are no timelines self.timeline_list.activate_next() self.assert_null_index() # does not change if its the last self.append_timeline() self.assertEqual(self.timeline_list.active_index, 0) self.timeline_list.activate_next() self.assertEqual(self.timeline_list.active_index, 0) def test_activate_previous_and_activate_next(self): self.append_timeline() self.append_timeline() self.append_timeline() # next self.timeline_list.activate_next() self.assertEqual(self.timeline_list.active_index, 1) self.timeline_list.activate_next() self.assertEqual(self.timeline_list.active_index, 2) # previous self.timeline_list.activate_previous() self.assertEqual(self.timeline_list.active_index, 1) self.timeline_list.activate_previous() self.assertEqual(self.timeline_list.active_index, 0) def test_active_returns_first_appended(self): # append name = 'Timeline' timeline = Timeline(name) self.timeline_list.append_timeline(timeline) # assert active_timeline = self.timeline_list.active self.assertEqual(timeline, active_timeline) def test_active_returns_None_when_empty(self): self.assertEqual(self.timeline_list.active, None) def test_append_timeline_increases_timeline_size(self): self.assertEqual(len(self.timeline_list), 0) self.append_timeline() self.assertEqual(len(self.timeline_list), 1) self.append_timeline() self.assertEqual(len(self.timeline_list), 2) def test_activate_first(self): # null index when there are no timelines self.timeline_list.activate_first() self.assert_null_index() # does not change if its the first self.append_timeline() self.assertEqual(self.timeline_list.active_index, 0) self.timeline_list.activate_first() self.assertEqual(self.timeline_list.active_index, 0) # moves to the first when in another position self.append_timeline() self.append_timeline() self.timeline_list.activate_next() self.timeline_list.activate_next() self.timeline_list.activate_first() self.assertEqual(self.timeline_list.active_index, 0) def test_activate_last(self): # null index when there are no timelines self.timeline_list.activate_last() self.assert_null_index() # does not change if its the last self.append_timeline() self.assertEqual(self.timeline_list.active_index, 0) self.timeline_list.activate_last() self.assertEqual(self.timeline_list.active_index, 0) # moves to the last when in another position self.append_timeline() self.append_timeline() self.timeline_list.activate_last() self.assertEqual(self.timeline_list.active_index, 2) def test_shift_active_previous(self): # null index when there are no timelines self.timeline_list.shift_active_previous() self.assert_null_index() # does not change if its the first self.append_timeline() self.timeline_list.shift_active_previous() self.assertEqual(self.timeline_list.active_index, 0) def test_shift_active_next(self): # null index when there are no timelines self.timeline_list.shift_active_next() self.assert_null_index() # does not change if its the last self.append_timeline() self.timeline_list.shift_active_next() self.assertEqual(self.timeline_list.active_index, 0) # it increases until reaching the end self.append_timeline() self.timeline_list.shift_active_next() self.assertEqual(self.timeline_list.active_index, 1) self.append_timeline() self.timeline_list.shift_active_next() self.assertEqual(self.timeline_list.active_index, 2) self.timeline_list.shift_active_next() self.assertEqual(self.timeline_list.active_index, 2) # visibility def test_no_visible_when_newly_created(self): self.assert_visible([]) self.timeline_list.expand_visible_previous() def test_only_visible_is_index_0_when_appending_first_timeline(self): self.append_timeline() self.assert_visible([0]) def test_expand_visible_previous(self): self.append_timeline() self.append_timeline() self.append_timeline() self.assert_visible([0]) self.timeline_list.activate_last() self.assert_visible([2]) self.timeline_list.expand_visible_previous() self.assert_visible([1, 2]) self.timeline_list.expand_visible_previous() self.assert_visible([0, 1, 2]) # there are no more timelines self.timeline_list.expand_visible_previous() self.assert_visible([0, 1, 2]) def test_expand_visible_next(self): self.append_timeline() self.append_timeline() self.append_timeline() self.assert_visible([0]) self.timeline_list.expand_visible_next() self.assert_visible([0, 1]) self.timeline_list.expand_visible_next() self.assert_visible([0, 1, 2]) # there are no more timelines self.timeline_list.expand_visible_next() self.assert_visible([0, 1, 2]) def test_shrink_visible_beggining(self): self.append_timeline() self.append_timeline() self.append_timeline() self.timeline_list.activate_last() self.timeline_list.expand_visible_previous() self.timeline_list.expand_visible_previous() self.assert_visible([0, 1, 2]) self.timeline_list.shrink_visible_beggining() self.assert_visible([1, 2]) self.timeline_list.shrink_visible_beggining() self.assert_visible([2]) # at least the active timeline has to be visible self.timeline_list.shrink_visible_beggining() self.assert_visible([2]) def test_shrink_visible_end(self): self.append_timeline() self.append_timeline() self.append_timeline() self.timeline_list.expand_visible_next() self.timeline_list.expand_visible_next() self.assert_visible([0, 1, 2]) self.timeline_list.shrink_visible_end() self.assert_visible([0, 1]) self.timeline_list.shrink_visible_end() self.assert_visible([0]) # at least the active timeline has to be visible self.timeline_list.shrink_visible_end() self.assert_visible([0]) def test_visible_active_only_when_activating_invisible_timeline(self): self.append_timeline() self.append_timeline() self.append_timeline() self.timeline_list.expand_visible_next() self.assert_visible([0, 1]) self.timeline_list.activate_last() self.assert_visible([2]) self.timeline_list.expand_visible_previous() self.assert_visible([1, 2]) self.timeline_list.activate_first() self.assert_visible([0]) def test_consistent_visible_timelines_when_deleting_leftmost_active(self): self.append_timeline() self.append_timeline() self.append_timeline() self.timeline_list.expand_visible_next() self.timeline_list.expand_visible_next() self.assertEqual(self.active_index(), 0) self.assert_visible([0, 1, 2]) self.timeline_list.delete_active_timeline() # active index does not change self.assertEqual(self.active_index(), 0) # visible self.assert_visible([0, 1]) # relative index relative_index = self.timeline_list.active_index_relative_to_visible self.assertEqual(relative_index, 0) def test_consistent_visible_timelines_when_deleting_middle_active(self): self.append_timeline() self.append_timeline() self.append_timeline() self.append_timeline() self.append_timeline() self.timeline_list.expand_visible_next() self.timeline_list.expand_visible_next() self.timeline_list.expand_visible_next() self.timeline_list.expand_visible_next() self.timeline_list.expand_visible_next() self.assertEqual(self.active_index(), 0) self.assert_visible([0, 1, 2, 3, 4]) self.timeline_list.activate_next() self.timeline_list.activate_next() self.assertEqual(self.active_index(), 2) self.assert_visible([0, 1, 2, 3, 4]) self.timeline_list.delete_active_timeline() # active index does not change self.assertEqual(self.active_index(), 2) # visible self.assert_visible([0, 1, 2, 3]) # relative index relative_index = self.timeline_list.active_index_relative_to_visible self.assertEqual(relative_index, 2) def test_consistent_visible_timelines_when_deleting_rightmost_timeline( self): self.append_timeline() self.append_timeline() self.append_timeline() self.assertEqual(len(self.timeline_list), 3) self.timeline_list.expand_visible_next() self.timeline_list.expand_visible_next() self.assertEqual(self.active_index(), 0) self.assert_visible([0, 1, 2]) self.timeline_list.activate_next() self.timeline_list.activate_next() self.assertEqual(self.active_index(), 2) self.assert_visible([0, 1, 2]) self.timeline_list.delete_active_timeline() self.assertEqual(len(self.timeline_list), 2) # active index shifts left self.assertEqual(self.active_index(), 1) # visible self.assert_visible([0, 1]) # relative index relative_index = self.timeline_list.active_index_relative_to_visible self.assertEqual(relative_index, 1) def test_delete_active_timeline_with_one_visible_timeline_in_the_left( self): self.append_timeline() self.append_timeline() self.assertEqual(self.active_index(), 0) self.assert_visible([0]) self.timeline_list.delete_active_timeline() self.assertEqual(self.active_index(), 0) self.assert_visible([0]) def test_delete_active_timeline_with_one_visible_timeline_in_the_middle( self): self.append_timeline() self.append_timeline() self.append_timeline() self.append_timeline() self.append_timeline() self.assertEqual(self.active_index(), 0) self.assert_visible([0]) self.assertEqual(len(self.timeline_list), 5) self.timeline_list.activate_next() self.timeline_list.activate_next() self.assertEqual(self.active_index(), 2) self.assert_visible([2]) self.timeline_list.delete_active_timeline() self.assertEqual(len(self.timeline_list), 4) self.assertEqual(self.active_index(), 2) self.assert_visible([2]) def test_delete_active_timeline_with_one_visible_timeline_in_the_right( self): self.append_timeline() self.append_timeline() self.append_timeline() self.assertEqual(len(self.timeline_list), 3) self.timeline_list.activate_next() self.timeline_list.activate_next() self.assertEqual(self.active_index(), 2) self.assert_visible([2]) self.timeline_list.delete_active_timeline() self.assertEqual(len(self.timeline_list), 2) self.assertEqual(self.active_index(), 1) self.assert_visible([1])
class Controller(object): """ An abstract class that implements most of the controller logic of ``turses``. Subclasses must implement the methods decorated with ``abc.abstractmethod``. """ INFO_MODE = 0 TIMELINE_MODE = 1 HELP_MODE = 2 EDITOR_MODE = 3 USER_INFO_MODE = 4 __metaclass__ = ABCMeta # -- Initialization ------------------------------------------------------- def __init__(self, configuration, ui, api_backend): self.configuration = configuration self.ui = ui self.editor = None self.timelines = TimelineList() # Mode self.mode = self.INFO_MODE # API oauth_token = self.configuration.oauth_token oauth_token_secret = self.configuration.oauth_token_secret self.api = AsyncApi(api_backend, access_token_key=oauth_token, access_token_secret=oauth_token_secret,) def start(self): self.authenticate_api() self.main_loop() def authenticate_api(self): self.info_message(_('Authenticating API')) self.api.init_api(on_error=self.api_init_error, on_success=self.init_timelines,) @async def init_timelines(self): # API has to be authenticated while (not self.api.is_authenticated): pass self.user = self.api.verify_credentials() self.info_message(_('Initializing timelines')) self.append_default_timelines() seconds = self.configuration.update_frequency # The main loop must have started while (not hasattr(self, 'loop')): pass # TODO: don't call `loop` explicitly or pull down to `CursesController` self.loop.set_alarm_in(seconds, self.update_alarm) # abstract methods @abstractmethod def main_loop(self): """ Main loop of the program, `Controller` subclasses must override this method. """ pass @abstractmethod def exit(self): """Exit the program.""" pass # -- Callbacks ------------------------------------------------------------ def api_init_error(self): # TODO retry self.error_message(_('Couldn\'t initialize API')) def update_alarm(self, *args, **kwargs): seconds = self.configuration.update_frequency self.update_all_timelines() self.loop.set_alarm_in(seconds, self.update_alarm) # -- Modes ---------------------------------------------------------------- def timeline_mode(self): """ Activates the Timeline mode if there are Timelines. If not, shows program info. """ if self.is_in_user_info_mode(): self.ui.hide_user_info() if self.is_in_timeline_mode(): return if self.is_in_help_mode(): self.clear_status() if self.timelines.has_timelines(): self.mode = self.TIMELINE_MODE self.draw_timelines() else: self.mode = self.INFO_MODE self.ui.show_info() self.redraw_screen() def is_in_timeline_mode(self): return self.mode == self.TIMELINE_MODE def info_mode(self): self.mode = self.INFO_MODE self.ui.show_info() self.redraw_screen() def is_in_info_mode(self): return self.mode == self.INFO_MODE def help_mode(self): """Activate help mode.""" if self.is_in_help_mode(): return self.mode = self.HELP_MODE self.ui.show_help(self.configuration) self.redraw_screen() def is_in_help_mode(self): return self.mode == self.HELP_MODE def editor_mode(self, editor): """Activate editor mode.""" self.editor = editor self.mode = self.EDITOR_MODE def is_in_editor_mode(self): return self.mode == self.EDITOR_MODE def user_info_mode(self, user): """Activate user info mode.""" self._user_info = user self.mode = self.USER_INFO_MODE def is_in_user_info_mode(self): return self.mode == self.USER_INFO_MODE # -- Timelines ------------------------------------------------------------ @wrap_exceptions def append_timeline(self, name, update_function, update_args=None, update_kwargs=None): """ Given a name, function to update a timeline and optionally arguments to the update function, it creates the timeline and appends it to `timelines`. """ timeline = Timeline(name=name, update_function=update_function, update_function_args=update_args, update_function_kwargs=update_kwargs) timeline.update() timeline.activate_first() self.timelines.append_timeline(timeline) if self.is_in_info_mode(): self.timeline_mode() self.draw_timelines() @async def append_default_timelines(self): default_timelines = { HOME_TIMELINE: self.append_home_timeline, MENTIONS_TIMELINE: self.append_mentions_timeline, FAVORITES_TIMELINE: self.append_favorites_timeline, MESSAGES_TIMELINE: self.append_direct_messages_timeline, OWN_TWEETS_TIMELINE: self.append_own_tweets_timeline, } timelines = [ HOME_TIMELINE, MENTIONS_TIMELINE, FAVORITES_TIMELINE, MESSAGES_TIMELINE, OWN_TWEETS_TIMELINE, ] is_any = any([self.configuration.default_timelines[timeline] for timeline in timelines]) if is_any: self.timeline_mode() else: message = _('You don\'t have any default timelines activated') self.info_message(message) return for timeline in timelines: append = default_timelines[timeline] if self.configuration.default_timelines[timeline]: append() self.draw_timelines() self.clear_status() def append_home_timeline(self): timeline_fetched = partial(self.info_message, _('Home timeline fetched')) timeline_not_fetched = partial(self.error_message, _('Failed to fetch home timeline')) self.append_timeline(name=_('tweets'), update_function=self.api.get_home_timeline, on_error=timeline_not_fetched, on_success=timeline_fetched,) def append_user_timeline(self, username): success_message = _('@%s\'s tweets fetched' % username) timeline_fetched = partial(self.info_message, success_message) error_message = _('Failed to fetch @%s\'s tweets' % username) timeline_not_fetched = partial(self.error_message, error_message) self.append_timeline(name='@%s' % username, update_function=self.api.get_user_timeline, update_kwargs={'screen_name': username}, on_error=timeline_not_fetched, on_success=timeline_fetched,) def append_own_tweets_timeline(self): timeline_fetched = partial(self.info_message, _('Your tweets fetched')) timeline_not_fetched = partial(self.error_message, _('Failed to fetch your tweets')) if not hasattr(self, 'user'): self.user = self.api.verify_credentials() self.append_timeline(name='@%s' % self.user.screen_name, update_function=self.api.get_own_timeline, on_error=timeline_not_fetched, on_success=timeline_fetched,) def append_mentions_timeline(self): timeline_fetched = partial(self.info_message, _('Mentions fetched')) timeline_not_fetched = partial(self.error_message, _('Failed to fetch mentions')) self.append_timeline(name=_('mentions'), update_function=self.api.get_mentions, on_error=timeline_not_fetched, on_success=timeline_fetched,) def append_favorites_timeline(self): timeline_fetched = partial(self.info_message, _('Favorites fetched')) timeline_not_fetched = partial(self.error_message, _('Failed to fetch favorites')) self.append_timeline(name=_('favorites'), update_function=self.api.get_favorites, on_error=timeline_not_fetched, on_success=timeline_fetched,) def append_direct_messages_timeline(self): timeline_fetched = partial(self.info_message, _('Messages fetched')) timeline_not_fetched = partial(self.error_message, _('Failed to fetch messages')) self.append_timeline(name=_('messages'), update_function=self.api.get_direct_messages, on_error=timeline_not_fetched, on_success=timeline_fetched,) def append_thread_timeline(self): status = self.timelines.active_status timeline_fetched = partial(self.info_message, _('Thread fetched')) timeline_not_fetched = partial(self.error_message, _('Failed to fetch thread')) if is_DM(status): self.error_message(_('Doesn\'t look like a public conversation')) return participants = status.mentioned_usernames author = status.authors_username if author not in participants: participants.insert(0, author) name = _('thread: %s' % ', '.join(participants)) self.append_timeline(name=name, update_function=self.api.get_thread, update_args=status, on_error=timeline_not_fetched, on_success=timeline_fetched) @async def update_all_timelines(self): for timeline in self.timelines: timeline.update() self.draw_timelines() self.info_message(_('%s updated' % timeline.name)) self.clear_status() # -- Timeline mode -------------------------------------------------------- def draw_timelines(self): if self.is_in_timeline_mode(): self.update_header() self.draw_timeline_buffer() def update_header(self): template = self.configuration.styles['tab_template'] name_and_unread = [(tl.name, tl.unread_count) for tl in self.timelines] tabs = [template.format(timeline_name=name, unread=unread) for (name, unread) in name_and_unread] self.ui.set_tab_names(tabs) # highlight the active active_index = self.timelines.active_index self.ui.activate_tab(active_index) # colorize the visible tabs visible_indexes = self.timelines.visible self.ui.header.set_visible_tabs(visible_indexes) def draw_timeline_buffer(self): # draw visible timelines visible_timelines = self.timelines.visible_timelines self.ui.draw_timelines(visible_timelines) # focus active timeline active_timeline = self.timelines.active active_pos = self.timelines.active_index_relative_to_visible # focus active status self.ui.focus_timeline(active_pos) self.ui.focus_status(active_timeline.active_index) def mark_all_as_read(self): """Mark all statuses in active timeline as read.""" active_timeline = self.timelines.active for tweet in active_timeline: tweet.read = True self.update_header() @async def update_active_timeline(self): """Updates the timeline and renders the active timeline.""" if self.timelines.has_timelines(): active_timeline = self.timelines.active try: newest = active_timeline[0] except IndexError: return active_timeline.update(since_id=newest.id) if self.is_in_timeline_mode(): self.draw_timelines() self.info_message('%s updated' % active_timeline.name) @async def update_active_timeline_with_newer_statuses(self): """ Updates the active timeline with newer tweets than the active. """ active_timeline = self.timelines.active active_status = active_timeline.active if active_status: active_timeline.update(since_id=active_status.id) @async def update_active_timeline_with_older_statuses(self): """ Updates the active timeline with older tweets than the active. """ active_timeline = self.timelines.active active_status = active_timeline.active if active_status: active_timeline.update(max_id=active_status.id) def previous_timeline(self): if self.timelines.has_timelines(): self.timelines.activate_previous() self.draw_timelines() def next_timeline(self): if self.timelines.has_timelines(): self.timelines.activate_next() self.draw_timelines() def shift_buffer_left(self): if self.timelines.has_timelines(): self.timelines.shift_active_previous() self.draw_timelines() def shift_buffer_right(self): if self.timelines.has_timelines(): self.timelines.shift_active_next() self.draw_timelines() def shift_buffer_beggining(self): if self.timelines.has_timelines(): self.timelines.shift_active_beggining() self.draw_timelines() def shift_buffer_end(self): if self.timelines.has_timelines(): self.timelines.shift_active_end() self.draw_timelines() def expand_buffer_left(self): if self.timelines.has_timelines(): self.timelines.expand_visible_previous() self.draw_timelines() def expand_buffer_right(self): if self.timelines.has_timelines(): self.timelines.expand_visible_next() self.draw_timelines() def shrink_buffer_left(self): if self.timelines.has_timelines(): self.timelines.shrink_visible_beggining() self.draw_timelines() def shrink_buffer_right(self): if self.timelines.has_timelines(): self.timelines.shrink_visible_end() self.draw_timelines() def activate_first_buffer(self): if self.timelines.has_timelines(): self.timelines.activate_first() self.draw_timelines() def activate_last_buffer(self): if self.timelines.has_timelines(): self.timelines.activate_last() self.draw_timelines() def delete_buffer(self): self.timelines.delete_active_timeline() if self.timelines.has_timelines(): self.draw_timelines() else: self.info_mode() # -- Motion --------------------------------------------------------------- def scroll_up(self): self.ui.focus_previous() if self.is_in_timeline_mode(): active_timeline = self.timelines.active # update with newer tweets when scrolling down being at the bottom if active_timeline.active_index == 0: self.update_active_timeline_with_newer_statuses() active_timeline.activate_previous() self.draw_timelines() def scroll_down(self): self.ui.focus_next() if self.is_in_timeline_mode(): active_timeline = self.timelines.active # update with older tweets when scrolling down being at the bottom if active_timeline.active_index == len(active_timeline) - 1: self.update_active_timeline_with_older_statuses() active_timeline.activate_next() self.draw_timelines() def scroll_top(self): self.ui.focus_first() if self.is_in_timeline_mode(): active_timeline = self.timelines.active active_timeline.activate_first() self.draw_timelines() def scroll_bottom(self): self.ui.focus_last() if self.is_in_timeline_mode(): active_timeline = self.timelines.active active_timeline.activate_last() self.draw_timelines() # -- Footer --------------------------------------------------------------- def error_message(self, message): self.ui.status_error_message(message) self.redraw_screen() def info_message(self, message): self.ui.status_info_message(message) self.redraw_screen() def clear_body(self): """Clear body.""" self.ui.body.clear() def clear_status(self): """Clear the status bar.""" self.ui.clear_status() self.redraw_screen() # -- UI ------------------------------------------------------------------- @abstractmethod def redraw_screen(self): pass # -- Editor --------------------------------------------------------------- def forward_to_editor(self, key): if self.editor: size = 20, self.editor.keypress(size, key) @text_from_editor def tweet_handler(self, text): """Handle the post as a tweet of the given `text`.""" self.info_message(_('Sending tweet')) if not is_valid_status_text(text): # <Esc> was pressed self.info_message(_('Tweet canceled')) return tweet_sent = partial(self.info_message, _('Tweet sent')) tweet_not_sent = partial(self.error_message, _('Tweet not sent')) # API call self.api.update(text=text, on_success=tweet_sent, on_error=tweet_not_sent,) @text_from_editor def direct_message_handler(self, username, text): """Handle the post as a DM of the given `text` to `username`.""" self.info_message(_('Sending DM')) if not is_valid_status_text(text): # <Esc> was pressed self.info_message(_('DM canceled')) return dm_info = _('Direct Message to @%s sent' % username) dm_sent = partial(self.info_message, dm_info) dm_error = _('Failed to send message to @%s' % username) dm_not_sent = partial(self.error_message, dm_error) self.api.direct_message(screen_name=username, text=text, on_success=dm_sent, on_error=dm_not_sent,) @text_from_editor def follow_user_handler(self, username): """ Handles following the user given in `username`. """ if username is None: self.info_message(_('Search cancelled')) return username = sanitize_username(username) if username == self.user.screen_name: self.error_message(_('You can\'t follow yourself')) return # TODO make sure that the user EXISTS and THEN follow if not is_username(username): self.info_message(_('Invalid username')) return else: self.info_message(_('Following @%s' % username)) success_message = _('You are now following @%s' % username) follow_done = partial(self.info_message, success_message) error_template = _('We can not ensure that you are following @%s') error_message = error_template % username follow_error = partial(self.error_message, error_message) self.api.create_friendship(screen_name=username, on_error=follow_error, on_success=follow_done) @text_from_editor def unfollow_user_handler(self, username): """ Handles unfollowing the user given in `username`. """ if username is None: self.info_message(_('Search cancelled')) return username = sanitize_username(username) if username == self.user.screen_name: self.error_message(_('That doesn\'t make any sense')) return # TODO make sure that the user EXISTS and THEN follow if not is_username(username): self.info_message(_('Invalid username')) return else: self.info_message(_('Unfollowing @%s' % username)) success_message = _('You are no longer following %s' % username) unfollow_done = partial(self.info_message, success_message) error_template = _('We can not ensure that you are not following %s') error_message = error_template % username unfollow_error = partial(self.error_message, error_message) self.api.destroy_friendship(screen_name=username, on_error=unfollow_error, on_success=unfollow_done) @text_from_editor def search_handler(self, text): """ Handles creating a timeline tracking the search term given in `text`. """ if text is None: self.info_message(_('Search cancelled')) return text = text.strip() if not is_valid_search_text(text): self.error_message(_('Invalid search')) return else: self.info_message(_('Creating search timeline for "%s"' % text)) success_message = _('Search timeline for "%s" created' % text) timeline_created = partial(self.info_message, success_message) error_message = _('Error creating search timeline for "%s"' % text) timeline_not_created = partial(self.info_message, error_message) self.append_timeline(name=_('Search: %s' % text), update_function=self.api.search, update_args=text, on_error=timeline_not_created, on_success=timeline_created) @text_from_editor def search_user_handler(self, username): """ Handles creating a timeline tracking the searched user's tweets. """ if username is None: self.info_message(_('Search cancelled')) return # TODO make sure that the user EXISTS and THEN fetch its tweets username = sanitize_username(username) if not is_username(username): self.info_message(_('Invalid username')) return else: self.info_message(_('Fetching latest tweets from @%s' % username)) success_message = _('@%s\'s timeline created' % username) timeline_created = partial(self.info_message, success_message) error_message = _('Unable to create @%s\'s timeline' % username) timeline_not_created = partial(self.error_message, error_message) self.append_timeline(name='@%s' % username, update_function=self.api.get_user_timeline, update_args=username, on_success=timeline_created, on_error=timeline_not_created) # -- Twitter -------------------------------------------------------------- def search(self, text=None): text = '' if text is None else text handler = self.search_handler editor = self.ui.show_text_editor(prompt='Search', content=text, done_signal_handler=handler) self.editor_mode(editor) def search_user(self): prompt = _('Search user (no need to prepend it with "@"') handler = self.search_user_handler editor = self.ui.show_text_editor(prompt=prompt, content='', done_signal_handler=handler) self.editor_mode(editor) @has_active_status def search_hashtags(self): status = self.timelines.active_status hashtags = ' '.join(status.hashtags) self.search_handler(text=hashtags) @has_active_status def focused_status_author_timeline(self): status = self.timelines.active_status author = status.authors_username self.append_user_timeline(author) def tweet(self, prompt=_('Tweet'), content='', cursor_position=None): handler = self.tweet_handler editor = self.ui.show_tweet_editor(prompt=prompt, content=content, done_signal_handler=handler, cursor_position=cursor_position) self.editor_mode(editor) @has_active_status def retweet(self): status = self.timelines.active_status if is_DM(status): self.error_message(_('You can\'t retweet direct messages')) return retweet_posted = partial(self.info_message, _('Retweet posted')) retweet_post_failed = partial(self.error_message, _('Failed to post retweet')) self.api.retweet(on_error=retweet_post_failed, on_success=retweet_posted, status=status,) @has_active_status def manual_retweet(self): status = self.timelines.active_status rt_text = ''.join([' RT @%s: ' % status.authors_username, status.text]) if is_valid_status_text(rt_text): self.tweet(content=rt_text, cursor_position=0) else: self.error_message(_('Tweet too long for manual retweet')) @has_active_status def reply(self): status = self.timelines.active_status if is_DM(status): self.direct_message() return author = status.authors_username mentioned = status.mentioned_for_reply try: mentioned.remove('@%s' % self.user.screen_name) except ValueError: pass handler = self.tweet_handler editor = self.ui.show_tweet_editor(prompt=_('Reply to %s' % author), content=' '.join(mentioned), done_signal_handler=handler) self.editor_mode(editor) @has_active_status def direct_message(self): status = self.timelines.active_status recipient = status.dm_recipients_username(self.user.screen_name) if recipient: handler = self.direct_message_handler editor = self.ui.show_dm_editor(prompt=_('DM to %s' % recipient), content='', recipient=recipient, done_signal_handler=handler) self.editor_mode(editor) else: self.error_message(_('What do you mean?')) @has_active_status def tweet_with_hashtags(self): status = self.timelines.active_status hashtags = ' '.join(status.hashtags) if hashtags: handler = self.tweet_handler content = ''.join([' ', hashtags]) editor = self.ui.show_tweet_editor(prompt=_('%s' % hashtags), content=content, done_signal_handler=handler, cursor_position=0) self.editor_mode(editor) @has_active_status def delete_tweet(self): status = self.timelines.active_status if is_DM(status): self.delete_dm() return author = status.authors_username if (author != self.user.screen_name and status.user != self.user.screen_name): self.error_message(_('You can only delete your own tweets')) return status_deleted = partial(self.info_message, _('Tweet deleted')) status_not_deleted = partial(self.error_message, _('Failed to delete tweet')) self.api.destroy_status(status=status, on_error=status_not_deleted, on_success=status_deleted) def delete_dm(self): dm = self.timelines.active_status if dm is None: return if dm.sender_screen_name != self.user.screen_name: self.error_message(_('You can only delete messages sent by you')) return dm_deleted = partial(self.info_message, _('Message deleted')) dm_not_deleted = partial(self.error_message, _('Failed to delete message')) self.api.destroy_direct_message(status=dm, on_error=dm_not_deleted, on_success=dm_deleted) @has_active_status def follow_selected(self): status = self.timelines.active_status username = status.authors_username if username == self.user.screen_name: self.error_message(_('You can\'t follow yourself')) return success_message = _('You are now following @%s' % username) follow_done = partial(self.info_message, success_message) error_template = _('We can not ensure that you are following @%s') error_message = error_template % username follow_error = partial(self.error_message, error_message) self.api.create_friendship(screen_name=username, on_error=follow_error, on_success=follow_done) def follow_user(self, prompt=_('Follow user (no need to prepend it with "@"'), content='', cursor_position=None): handler = self.follow_user_handler editor = self.ui.show_text_editor(prompt=prompt, content=content, done_signal_handler=handler, cursor_position=cursor_position) self.editor_mode(editor) def unfollow_user(self, prompt=_('Unfollow user (no need to prepend it with' ' "@"'), content='', cursor_position=None): handler = self.unfollow_user_handler editor = self.ui.show_text_editor(prompt=prompt, content=content, done_signal_handler=handler, cursor_position=cursor_position) self.editor_mode(editor) @has_active_status def unfollow_selected(self): status = self.timelines.active_status username = status.authors_username if username == self.user.screen_name: self.error_message(_('That doesn\'t make any sense')) return success_message = _('You are no longer following %s' % username) unfollow_done = partial(self.info_message, success_message) error_template = _('We can not ensure that you are not following %s') error_message = error_template % username unfollow_error = partial(self.error_message, error_message) self.api.destroy_friendship(screen_name=username, on_error=unfollow_error, on_success=unfollow_done) @has_active_status def favorite(self): status = self.timelines.active_status favorite_error = partial(self.error_message, _('Failed to mark tweet as favorite')) favorite_done = partial(self.info_message, _('Tweet marked as favorite')) self.api.create_favorite(on_error=favorite_error, on_success=favorite_done, status=status,) @has_active_status def unfavorite(self): status = self.timelines.active_status unfavorite_error = partial(self.error_message, _('Failed to remove tweet from favorites')) unfavorite_done = partial(self.info_message, _('Tweet removed from favorites')) self.api.destroy_favorite(on_error=unfavorite_error, on_success=unfavorite_done, status=status,) @has_active_status def user_info(self): status = self.timelines.active_status user = self.api.get_user(status.authors_username) self.ui.show_user_info(user) self.user_info_mode(user) # - Configuration --------------------------------------------------------- def reload_configuration(self): self.configuration.reload() self.redraw_screen() self.info_message(_('Configuration reloaded')) # - Browser --------------------------------------------------------------- @has_active_status def open_urls(self): """ Open the URLs contained on the focused tweets in a browser. """ status = self.timelines.active_status urls = get_urls(status.text) if not urls: self.info_message(_('No URLs found on this tweet')) return args = ' '.join(urls) self.open_urls_in_browser(args) @has_active_status def open_status_url(self): """ Open the focused tweet in a browser. """ status = self.timelines.active_status if is_DM(status): message = _('You only can open regular statuses in a browser') self.info_message(message) return self.open_urls_in_browser(status.url) def open_urls_in_browser(self, urls): """ Open `urls` in $BROWSER if the environment variable is set. """ command = self.configuration.browser if not command: message = _('You have to set the BROWSER environment variable' ' to open URLs') self.error_message(message) return try: spawn_process(command, urls) except: self.error_message(_('Unable to launch the browser'))
class TimelineListTest(unittest.TestCase): def setUp(self): self.timeline_list = TimelineList() def append_timeline(self): self.timeline_list.append_timeline(Timeline('Timeline')) def test_has_timelines_false_if_empty(self): self.failIf(self.timeline_list.has_timelines()) def test_has_timelines_true_otherwise(self): self.append_timeline() self.failUnless(self.timeline_list.has_timelines()) def test_null_index_with_no_timelines(self): self.assertEqual(self.timeline_list.active_index, ActiveList.NULL_INDEX) def test_active_index_0_when_appending_first_timeline(self): self.append_timeline() self.assertEqual(self.timeline_list.active_index, 0) def test_activate_previous(self): # null index when there are no timelines self.timeline_list.activate_previous() self.assertEqual(self.timeline_list.active_index, ActiveList.NULL_INDEX) # does not change if its the first self.append_timeline() self.assertEqual(self.timeline_list.active_index, 0) self.timeline_list.activate_previous() self.assertEqual(self.timeline_list.active_index, 0) def test_activate_next(self): # null index when there are no timelines self.timeline_list.activate_next() self.assertEqual(self.timeline_list.active_index, ActiveList.NULL_INDEX) # does not change if its the last self.append_timeline() self.assertEqual(self.timeline_list.active_index, 0) self.timeline_list.activate_next() self.assertEqual(self.timeline_list.active_index, 0) def test_activate_previous_and_activate_next(self): self.append_timeline() self.append_timeline() self.append_timeline() # next self.timeline_list.activate_next() self.assertEqual(self.timeline_list.active_index, 1) self.timeline_list.activate_next() self.assertEqual(self.timeline_list.active_index, 2) # previous self.timeline_list.activate_previous() self.assertEqual(self.timeline_list.active_index, 1) self.timeline_list.activate_previous() self.assertEqual(self.timeline_list.active_index, 0) def test_get_active_timeline_name_returns_first_appended(self): # append name = 'Timeline' timeline = Timeline(name) self.timeline_list.append_timeline(timeline) # assert active_timeline_name = self.timeline_list.get_active_timeline_name() self.assertEqual(name, active_timeline_name) def test_get_active_timeline_name_raises_exception_when_empty(self): self.assertRaises(Exception, self.timeline_list.get_active_timeline_name) def test_get_active_timeline_returns_first_appended(self): # append name = 'Timeline' timeline = Timeline(name) self.timeline_list.append_timeline(timeline) # assert active_timeline = self.timeline_list.get_active_timeline() self.assertEqual(timeline, active_timeline) def test_get_active_timeline_raises_exception_when_empty(self): self.assertRaises(Exception, self.timeline_list.get_active_timeline) def test_append_timeline_increases_timeline_size(self): self.assertEqual(len(self.timeline_list), 0) self.append_timeline() self.assertEqual(len(self.timeline_list), 1) self.append_timeline() self.assertEqual(len(self.timeline_list), 2) def test_activate_first(self): # null index when there are no timelines self.timeline_list.activate_first() self.assertEqual(self.timeline_list.active_index, ActiveList.NULL_INDEX) # does not change if its the first self.append_timeline() self.assertEqual(self.timeline_list.active_index, 0) self.timeline_list.activate_first() self.assertEqual(self.timeline_list.active_index, 0) # moves to the first when in another position self.append_timeline() self.append_timeline() self.timeline_list.activate_next() self.timeline_list.activate_next() self.timeline_list.activate_first() self.assertEqual(self.timeline_list.active_index, 0) def test_activate_last(self): # null index when there are no timelines self.timeline_list.activate_last() self.assertEqual(self.timeline_list.active_index, ActiveList.NULL_INDEX) # does not change if its the last self.append_timeline() self.assertEqual(self.timeline_list.active_index, 0) self.timeline_list.activate_last() self.assertEqual(self.timeline_list.active_index, 0) # moves to the last when in another position self.append_timeline() self.append_timeline() self.timeline_list.activate_last() self.assertEqual(self.timeline_list.active_index, 2) def test_shift_active_previous(self): # null index when there are no timelines self.timeline_list.shift_active_previous() self.assertEqual(self.timeline_list.active_index, ActiveList.NULL_INDEX) # does not change if its the first self.append_timeline() self.timeline_list.shift_active_previous() self.assertEqual(self.timeline_list.active_index, 0) def test_shift_active_next(self): # null index when there are no timelines self.timeline_list.shift_active_next() self.assertEqual(self.timeline_list.active_index, ActiveList.NULL_INDEX) # does not change if its the last self.append_timeline() self.timeline_list.shift_active_next() self.assertEqual(self.timeline_list.active_index, 0) # it increases until reaching the end self.append_timeline() self.timeline_list.shift_active_next() self.assertEqual(self.timeline_list.active_index, 1) self.append_timeline() self.timeline_list.shift_active_next() self.assertEqual(self.timeline_list.active_index, 2) self.timeline_list.shift_active_next() self.assertEqual(self.timeline_list.active_index, 2)