From 33c9dce80d7f2c7b778bb2d92568be5d04ab1fab Mon Sep 17 00:00:00 2001 From: Richard Schneider Date: Sat, 21 May 2022 14:03:27 -0500 Subject: [PATCH] Implement --change-time flag (#1452) * Implement --change-time flag * Remove todo from --change-time bdd folder journal tests * Add warning when --change-time used with no matching entries * Add a test to make sure running --change-time with nothing prints a warning and doesn't change anything * Add prompt for --change-time * Don't prompt for --change-time when used with --edit and only one entry * When using --edit and --change-time, change the time before editing * Add test for --change-time used with --edit * Modify failing --change-time test to conform to text in develop branch --- jrnl/FolderJournal.py | 12 ++ jrnl/Journal.py | 24 ++- jrnl/args.py | 8 + jrnl/jrnl.py | 57 +++++- jrnl/messages.py | 4 + tests/bdd/features/change_time.feature | 240 +++++++++++++++++++++++++ tests/bdd/test_features.py | 1 + tests/unit/test_parse_args.py | 8 + 8 files changed, 342 insertions(+), 12 deletions(-) create mode 100644 tests/bdd/features/change_time.feature diff --git a/jrnl/FolderJournal.py b/jrnl/FolderJournal.py index 74f6291bb..62b7ade5f 100644 --- a/jrnl/FolderJournal.py +++ b/jrnl/FolderJournal.py @@ -8,6 +8,7 @@ import os from . import Journal +from . import time def get_files(journal_config): @@ -89,6 +90,17 @@ def delete_entries(self, entries_to_delete): self.entries.remove(entry) self._diff_entry_dates.append(entry.date) + def change_date_entries(self, date): + """Changes entry dates to given date.""" + + date = time.parse(date) + + self._diff_entry_dates.append(date) + + for entry in self.entries: + self._diff_entry_dates.append(entry.date) + entry.date = date + def parse_editable_str(self, edited): """Parses the output of self.editable_str and updates its entries.""" mod_entries = self._parse(edited) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index bf446a927..13c5363a5 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -261,23 +261,29 @@ def delete_entries(self, entries_to_delete): for entry in entries_to_delete: self.entries.remove(entry) - def prompt_delete_entries(self): - """Prompts for deletion of each of the entries in a journal. - Returns the entries the user wishes to delete.""" + def change_date_entries(self, date): + """Changes entry dates to given date.""" + date = time.parse(date) - to_delete = [] + for entry in self.entries: + entry.date = date + + def prompt_action_entries(self, message): + """Prompts for action for each entry in a journal, using given message. + Returns the entries the user wishes to apply the action on.""" + to_act = [] - def ask_delete(entry): + def ask_action(entry): return yesno( - f"Delete entry '{entry.pprint(short=True)}'?", + f"{message} '{entry.pprint(short=True)}'?", default=False, ) for entry in self.entries: - if ask_delete(entry): - to_delete.append(entry) + if ask_action(entry): + to_act.append(entry) - return to_delete + return to_act def new_entry(self, raw, date=None, sort=True): """Constructs a new entry from some raw text input. diff --git a/jrnl/args.py b/jrnl/args.py index 6a2a86ea9..1f3e3c9ab 100644 --- a/jrnl/args.py +++ b/jrnl/args.py @@ -267,6 +267,14 @@ def parse_args(args=[]): action="store_true", help="Interactively deletes selected entries", ) + exporting.add_argument( + "--change-time", + dest="change_time", + nargs="?", + metavar="DATE", + const="now", + help="Change timestamp for seleted entries (default: now)", + ) exporting.add_argument( "--format", metavar="TYPE", diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index f6715f76d..7c09734e3 100644 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -79,6 +79,7 @@ def _is_write_mode(args, config, **kwargs): args.contains, args.delete, args.edit, + args.change_time, args.export, args.end_date, args.today_in_history, @@ -150,7 +151,9 @@ def write_mode(args, config, journal, **kwargs): def search_mode(args, journal, **kwargs): """ Search for entries in a journal, then either: - 1. Send them to configured editor for user manipulation + 1. Send them to configured editor for user manipulation (and also + change their timestamps if requested) + 2. Change their timestamps 2. Delete them (with confirmation for each entry) 3. Display them (with formatting options) """ @@ -166,8 +169,27 @@ def search_mode(args, journal, **kwargs): # Where do the search results go? if args.edit: + # If we want to both edit and change time in one action + if args.change_time: + # Generate a new list instead of assigning so it won't be + # modified by _change_time_search_results + selected_entries = [e for e in journal.entries] + + no_change_time_prompt = len(journal.entries) == 1 + _change_time_search_results(no_prompt=no_change_time_prompt, **kwargs) + + # Re-filter the journal enties (_change_time_search_results + # puts the filtered entries back); use selected_entries + # instead of running _search_journal again, because times + # have changed since the original search + kwargs["old_entries"] = journal.entries + journal.entries = selected_entries + _edit_search_results(**kwargs) + elif args.change_time: + _change_time_search_results(**kwargs) + elif args.delete: _delete_search_results(**kwargs) @@ -236,6 +258,11 @@ def _search_journal(args, journal, **kwargs): journal.limit(args.limit) +def _other_entries(journal, entries): + """Find entries that are not in journal""" + return [e for e in entries if e not in journal.entries] + + def _edit_search_results(config, journal, old_entries, **kwargs): """ 1. Send the given journal entries to the user-configured editor @@ -252,7 +279,7 @@ def _edit_search_results(config, journal, old_entries, **kwargs): ) # separate entries we are not editing - other_entries = [e for e in old_entries if e not in journal.entries] + other_entries = _other_entries(journal, old_entries) # Get stats now for summary later old_stats = _get_predit_stats(journal) @@ -309,7 +336,7 @@ def _delete_search_results(journal, old_entries, **kwargs): if not journal.entries: raise JrnlException(Message(MsgText.NothingToDelete, MsgType.ERROR)) - entries_to_delete = journal.prompt_delete_entries() + entries_to_delete = journal.prompt_action_entries("Delete entry") if entries_to_delete: journal.entries = old_entries @@ -318,6 +345,30 @@ def _delete_search_results(journal, old_entries, **kwargs): journal.write() +def _change_time_search_results(args, journal, old_entries, no_prompt=False, **kwargs): + if not journal.entries: + raise JrnlException(Message(MsgText.NothingToModify, MsgType.WARNING)) + + # separate entries we are not editing + other_entries = _other_entries(journal, old_entries) + + if no_prompt: + entries_to_change = journal.entries + else: + entries_to_change = journal.prompt_action_entries("Change time") + + if entries_to_change: + other_entries += [e for e in journal.entries if e not in entries_to_change] + journal.entries = entries_to_change + + date = time.parse(args.change_time) + journal.change_date_entries(date) + + journal.entries += other_entries + journal.sort() + journal.write() + + def _display_search_results(args, journal, **kwargs): if args.short or args.export == "short": print(journal.pprint(short=True)) diff --git a/jrnl/messages.py b/jrnl/messages.py index 486841957..34f45e7dc 100644 --- a/jrnl/messages.py +++ b/jrnl/messages.py @@ -130,6 +130,10 @@ def __str__(self) -> str: No entries to delete, because the search returned no results """ + NothingToModify = """ + No entries to modify, because the search returned no results + """ + class Message(NamedTuple): text: MsgText diff --git a/tests/bdd/features/change_time.feature b/tests/bdd/features/change_time.feature new file mode 100644 index 000000000..bd8e77f74 --- /dev/null +++ b/tests/bdd/features/change_time.feature @@ -0,0 +1,240 @@ +Feature: Change entry times in journal + Scenario Outline: Change time flag changes single entry timestamp + Given we use the config "" + And we use the password "test" if prompted + When we run "jrnl -1" + Then the output should contain "2020-09-24 09:14 The third entry finally" + When we run "jrnl -1 --change-time '2022-04-23 10:30'" and enter + Y + When we run "jrnl -99 --short" + Then the output should be + 2020-08-29 11:11 Entry the first. + 2020-08-31 14:32 A second entry in what I hope to be a long series. + 2022-04-23 10:30 The third entry finally after weeks without writing. + + Examples: Configs + | config_file | + | basic_onefile.yaml | + | basic_encrypted.yaml | + | basic_folder.yaml | + # | basic_dayone.yaml | @todo + + Scenario Outline: Change flag changes prompted entries + Given we use the config "" + And we use the password "test" if prompted + When we run "jrnl -1" + Then the output should contain "2020-09-24 09:14 The third entry finally" + When we run "jrnl --change-time '2022-04-23 10:30'" and enter + Y + N + Y + When we run "jrnl -99 --short" + Then the output should be + 2020-08-31 14:32 A second entry in what I hope to be a long series. + 2022-04-23 10:30 Entry the first. + 2022-04-23 10:30 The third entry finally after weeks without writing. + + Examples: Configs + | config_file | + | basic_onefile.yaml | + | basic_encrypted.yaml | + | basic_folder.yaml | + # | basic_dayone.yaml | @todo + + + Scenario Outline: Change time flag with nonsense input changes nothing + Given we use the config "" + When we run "jrnl --change-time now asdfasdf" + Then the output should contain "No entries to modify" + When we run "jrnl -99 --short" + Then the output should be + 2020-08-29 11:11 Entry the first. + 2020-08-31 14:32 A second entry in what I hope to be a long series. + 2020-09-24 09:14 The third entry finally after weeks without writing. + + Examples: Configs + | config_file | + | basic_onefile.yaml | + | basic_folder.yaml | + | basic_dayone.yaml | + + + Scenario Outline: Change time flag with tag only changes tagged entries + Given we use the config "" + When we run "jrnl --change-time '2022-04-23 10:30' @ipsum" and enter + Y + When we run "jrnl -99 --short" + Then the output should be + 2020-08-31 14:32 A second entry in what I hope to be a long series. + 2020-09-24 09:14 The third entry finally after weeks without writing. + 2022-04-23 10:30 Entry the first. + + Examples: Configs + | config_file | + | basic_onefile.yaml | + | basic_folder.yaml | + # | basic_dayone.yaml | @todo + + + Scenario Outline: Change time flag with multiple tags changes all entries matching any of the tags + Given we use the config "" + When we run "jrnl --change-time '2022-04-23 10:30' @ipsum @tagthree" and enter + Y + Y + When we run "jrnl -99 --short" + Then the output should be + 2020-08-31 14:32 A second entry in what I hope to be a long series. + 2022-04-23 10:30 Entry the first. + 2022-04-23 10:30 The third entry finally after weeks without writing. + + Examples: Configs + | config_file | + | basic_onefile.yaml | + | basic_folder.yaml | + # | basic_dayone.yaml | @todo + + + Scenario Outline: Change time flag with -and changes boolean AND of tagged entries + Given we use the config "" + When we run "jrnl --change-time '2022-04-23 10:30' -and @tagone @tagtwo" and enter + Y + When we run "jrnl -99 --short" + Then the output should be + 2020-08-31 14:32 A second entry in what I hope to be a long series. + 2020-09-24 09:14 The third entry finally after weeks without writing. + 2022-04-23 10:30 Entry the first. + + Examples: Configs + | config_file | + | basic_onefile.yaml | + | basic_folder.yaml | + # | basic_dayone.yaml | @todo + + + Scenario Outline: Change time flag with -not does not change entries from given tag + Given we use the config "" + When we run "jrnl --change-time '2022-04-23 10:30' @tagone -not @ipsum" and enter + Y + When we run "jrnl -99 --short" + Then the output should be + 2020-08-29 11:11 Entry the first. + 2020-08-31 14:32 A second entry in what I hope to be a long series. + 2022-04-23 10:30 The third entry finally after weeks without writing. + + Examples: Configs + | config_file | + | basic_onefile.yaml | + | basic_folder.yaml | + # | basic_dayone.yaml | @todo + + + Scenario Outline: Change time flag with -from search operator only changes entries since that date + Given we use the config "" + When we run "jrnl --change-time '2022-04-23 10:30' -from 2020-09-01" and enter + Y + When we run "jrnl -99 --short" + Then the output should be + 2020-08-29 11:11 Entry the first. + 2020-08-31 14:32 A second entry in what I hope to be a long series. + 2022-04-23 10:30 The third entry finally after weeks without writing. + + Examples: Configs + | config_file | + | basic_onefile.yaml | + | basic_folder.yaml | + # | basic_dayone.yaml | @todo + + + Scenario Outline: Change time flag with -to only changes entries up to specified date + Given we use the config "" + When we run "jrnl --change-time '2022-04-23 10:30' -to 2020-08-31" and enter + Y + Y + When we run "jrnl -99 --short" + Then the output should be + 2020-09-24 09:14 The third entry finally after weeks without writing. + 2022-04-23 10:30 Entry the first. + 2022-04-23 10:30 A second entry in what I hope to be a long series. + + Examples: Configs + | config_file | + | basic_onefile.yaml | + | basic_folder.yaml | + # | basic_dayone.yaml | @todo + + + Scenario Outline: Change time flag with -starred only changes starred entries + Given we use the config "" + When we run "jrnl --change-time '2022-04-23 10:30' -starred" and enter + Y + When we run "jrnl -99 --short" + Then the output should be + 2020-08-29 11:11 Entry the first. + 2020-09-24 09:14 The third entry finally after weeks without writing. + 2022-04-23 10:30 A second entry in what I hope to be a long series. + + Examples: Configs + | config_file | + | basic_onefile.yaml | + | basic_folder.yaml | + # | basic_dayone.yaml | @todo + + + Scenario Outline: Change time flag with -contains only changes entries containing expression + Given we use the config "" + When we run "jrnl --change-time '2022-04-23 10:30' -contains dignissim" and enter + Y + When we run "jrnl -99 --short" + Then the output should be + 2020-08-31 14:32 A second entry in what I hope to be a long series. + 2020-09-24 09:14 The third entry finally after weeks without writing. + 2022-04-23 10:30 Entry the first. + + Examples: Configs + | config_file | + | basic_onefile.yaml | + | basic_folder.yaml | + # | basic_dayone.yaml | @todo + + + Scenario Outline: Change time flag with no enties specified changes nothing + Given we use the config "" + And we use the password "test" if prompted + When we run "jrnl --change-time" and enter + N + N + N + When we run "jrnl -99 --short" + Then the output should be + 2020-08-29 11:11 Entry the first. + 2020-08-31 14:32 A second entry in what I hope to be a long series. + 2020-09-24 09:14 The third entry finally after weeks without writing. + + Examples: Configs + | config_file | + | basic_onefile.yaml | + | basic_folder.yaml | + | basic_dayone.yaml | + + + Scenario Outline: --change-time with --edit modifies selected entries + Given we use the config "" + And we write nothing to the editor if opened + And we use the password "test" if prompted + When we run "jrnl --change-time '2022-04-23 10:30' --edit" and enter + Y + N + Y + Then the error output should contain "No entry to save" + And the editor should have been called + When we run "jrnl -99 --short" + Then the output should be + 2020-08-31 14:32 A second entry in what I hope to be a long series. + 2022-04-23 10:30 Entry the first. + 2022-04-23 10:30 The third entry finally after weeks without writing. + + Examples: Configs + | config_file | + | basic_onefile.yaml | + | basic_folder.yaml | + # | basic_dayone.yaml | @todo diff --git a/tests/bdd/test_features.py b/tests/bdd/test_features.py index 5ef3506e7..04f9d46f5 100644 --- a/tests/bdd/test_features.py +++ b/tests/bdd/test_features.py @@ -5,6 +5,7 @@ scenarios("features/core.feature") scenarios("features/datetime.feature") scenarios("features/delete.feature") +scenarios("features/change_time.feature") scenarios("features/encrypt.feature") scenarios("features/file_storage.feature") scenarios("features/format.feature") diff --git a/tests/unit/test_parse_args.py b/tests/unit/test_parse_args.py index aadc6684b..7acae5711 100644 --- a/tests/unit/test_parse_args.py +++ b/tests/unit/test_parse_args.py @@ -17,6 +17,7 @@ def expected_args(**kwargs): "contains": None, "debug": False, "delete": False, + "change_time": None, "edit": False, "end_date": None, "today_in_history": False, @@ -58,6 +59,13 @@ def test_delete_alone(): assert cli_as_dict("--delete") == expected_args(delete=True) +def test_change_time_alone(): + assert cli_as_dict("--change-time") == expected_args(change_time="now") + assert cli_as_dict("--change-time yesterday") == expected_args( + change_time="yesterday" + ) + + def test_diagnostic_alone(): from jrnl.commands import preconfig_diagnostic