From 40eeb56a8cc37dc6c8f47a5170e29b4fb2ea6a38 Mon Sep 17 00:00:00 2001 From: GiantsLoveDeathMetal Date: Sun, 18 Feb 2018 01:34:38 +0000 Subject: [PATCH] FEA: Tag list type (#159) Resolves: #12 --- foolscap/actor.py | 6 ++--- foolscap/cli.py | 14 +++++++++- foolscap/note_display.py | 43 +++++++++++++++++++++++++++++-- tests/test_actor.py | 16 +++++++++--- tests/test_cli.py | 6 +++-- tests/test_note_display.py | 53 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 127 insertions(+), 11 deletions(-) diff --git a/foolscap/actor.py b/foolscap/actor.py index 8377df0..f600d48 100644 --- a/foolscap/actor.py +++ b/foolscap/actor.py @@ -25,7 +25,7 @@ } -def action(do_action, arg): +def action(do_action, arg, list_type='normal'): func = FUNCTION_MAP[do_action] new_action = None @@ -33,9 +33,9 @@ def action(do_action, arg): # Quitting from list calls exit() method. # arg is filter in this case if arg: - new_action = func(arg) + new_action = func(arg, list_type) else: - new_action = func(None) + new_action = func(None, list_type) if new_action: new_func, note = new_action diff --git a/foolscap/cli.py b/foolscap/cli.py index c460724..554cd2c 100644 --- a/foolscap/cli.py +++ b/foolscap/cli.py @@ -15,6 +15,11 @@ 'migrate', ] +LIST_TYPES = [ + 'tags', +] + + parser = argparse.ArgumentParser() parser.add_argument( 'command', @@ -25,12 +30,19 @@ action='store', nargs='?', ) +parser.add_argument( + '-t', + '--list_type', + default='normal', + choices=LIST_TYPES, +) def main(): args = parser.parse_args() command = args.command note_args = args.positional + list_type = args.list_type - action(command, note_args) + action(command, note_args, list_type) diff --git a/foolscap/note_display.py b/foolscap/note_display.py index dc592c1..c4b1fb2 100644 --- a/foolscap/note_display.py +++ b/foolscap/note_display.py @@ -1,5 +1,7 @@ -from foolscap.display.console import display_list +from collections import Counter, OrderedDict + from foolscap.meta_data import load_meta +from foolscap.display.console import display_list TOP_X_VIEWED = 3 # This rule is so perfectly subtle, @@ -9,7 +11,11 @@ SORT_BY_VIEWS_RULE = 5 -def list_notes(tags): +class OrderedCounter(Counter, OrderedDict): + pass + + +def list_notes(tags, list_type='normal'): """ Presents notes in the terminal. all_notes = { @@ -31,6 +37,9 @@ def list_notes(tags): for key, values in all_notes.items() if 'tags' in values and tags in values['tags'] } + if list_type == 'tags': + display_notes = create_tag_display(all_notes) + return display_list(display_notes) if len(all_notes) == 0: # Fuzzy here @@ -42,6 +51,36 @@ def list_notes(tags): return display_list(display_notes) +def count_tags(all_notes): + list_tags = [] + for key, values in all_notes.items(): + list_tags.extend(values['tags']) + return OrderedCounter(list_tags) + + +def get_by_tag(all_notes, tag): + notes = {key: values + for key, values in all_notes.items() + if 'tags' in values and tag in values['tags']} + notes = [(note, values['description']) for note, values in notes.items()] + return sorted(notes, key=lambda x: x[0].lower()) + + +def create_tag_display(all_notes): + display = [] + while all_notes: + tag_count = count_tags(all_notes) + max_tag, count = tag_count.most_common(1)[0] + display_tag = {} + display_tag['title'] = max_tag + display_tag['description'] = str(count) + display_tag['sub_headings'] = get_by_tag(all_notes, max_tag) + display.append(display_tag) + for key, _ in display_tag['sub_headings']: + all_notes.pop(key, 0) + return display + + def display_information(sorted_notes, note_dict): """ Get description from list of note titles""" display_notes = [] diff --git a/tests/test_actor.py b/tests/test_actor.py index ea94e47..0ab9ca8 100644 --- a/tests/test_actor.py +++ b/tests/test_actor.py @@ -57,7 +57,7 @@ def test_list_note_command_no_tags(): # This is tricky, as list calls exit() if it quits in menu object # but cause we don't call the menu object, we return None # and have to expect the func to be called again... - expected = [call(None), call()] + expected = [call(None, 'normal'), call()] # Pass to actor: actor.action('list', None) @@ -72,7 +72,7 @@ def test_list_note_command_tags(): # List with tags expects: # Note: same as last test. expected = [ - call('tag'), + call('tag', 'normal'), call('tag') ] @@ -92,7 +92,7 @@ def test_list_note_command_returning_func(): # If list function returns a new-action: mock_list.return_value = ('view', 'mock_note') - expected_list = call('tag') + expected_list = call('tag', 'normal') expected_view = call('mock_note') actor.action('list', 'tag') @@ -164,3 +164,13 @@ def test_all_actions(): assert set(ACTIONS) == set(TESTED_ACTIONS) +def test_change_list_type(): + mock_action = MagicMock() + with patch.dict('foolscap.actor.FUNCTION_MAP', {'list': mock_action}): + mock_action.return_value = None + expected = [ + call(None, 'tags'), + call() + ] + actor.action('list', None, 'tags') + assert mock_action.call_args_list == expected diff --git a/tests/test_cli.py b/tests/test_cli.py index cb783f0..ecdf364 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -22,7 +22,7 @@ def test_valid_command(): with patch('sys.argv', test_args),\ patch('foolscap.cli.action') as mock_action: - expected = call('save', 'mock_note.txt') + expected = call('save', 'mock_note.txt', 'normal') main() assert mock_action.call_args == expected @@ -34,7 +34,7 @@ def test_valid_command_optional(): with patch('sys.argv', test_args),\ patch('foolscap.cli.action') as mock_action: - expected = call('new', None) + expected = call('new', None, 'normal') main() assert mock_action.call_args == expected @@ -52,3 +52,5 @@ def test_invalid_command(): pytest.raises(SystemExit): main() + +# Add changing list type test diff --git a/tests/test_note_display.py b/tests/test_note_display.py index e9ee9a7..a39e505 100644 --- a/tests/test_note_display.py +++ b/tests/test_note_display.py @@ -137,3 +137,56 @@ def test_sort_notes(test_dict, expected): result = note_display.sort_notes(test_dict) assert result == expected + +def test_listing_tags(): + def fake_return(input_param): + return input_param + + with patch('foolscap.note_display.display_list') as mock_display,\ + patch('foolscap.note_display.load_meta') as mock_meta: + mock_meta.return_value = FAKE_MANY_NOTES.copy() + mock_display.side_effect = fake_return + result = note_display.list_notes(None, 'tags') + mock_display.assert_called_once() + assert result == [{'title': 'fake_tag', 'description': str(7), + 'sub_headings': [('A', 'This is a fake note'), + ('fake_note_1', 'This is a fake note'), + ('most_viewed', 'This is a fake note'), + ('recently_opened', 'This is a fake note'), + ('second_most', 'This is a fake note'), + ('third_most', 'This is a fake note'), + ('Z', 'This is a fake note')]}] + + +def test_count_tags(): + mock_notes = FAKE_MANY_NOTES.copy() + result = note_display.count_tags(mock_notes) + assert result.most_common(1)[0] == ('fake_tag', 7) + + +def test_get_by_tag(): + # Get all notes with tag: 'fake_tag' + mock_notes = FAKE_MANY_NOTES.copy() + result = note_display.get_by_tag(mock_notes, 'fake_tag') + assert result == [('A', 'This is a fake note'), + ('fake_note_1', 'This is a fake note'), + ('most_viewed', 'This is a fake note'), + ('recently_opened', 'This is a fake note'), + ('second_most', 'This is a fake note'), + ('third_most', 'This is a fake note'), + ('Z', 'This is a fake note')] + + +def test_create_tag_display(): + # Get all notes with tag: 'fake_tag' + mock_notes = FAKE_MANY_NOTES.copy() + result = note_display.create_tag_display(mock_notes) + assert result == [{'title': 'fake_tag', 'description': str(7), + 'sub_headings': [('A', 'This is a fake note'), + ('fake_note_1', 'This is a fake note'), + ('most_viewed', 'This is a fake note'), + ('recently_opened', 'This is a fake note'), + ('second_most', 'This is a fake note'), + ('third_most', 'This is a fake note'), + ('Z', 'This is a fake note')]}] +