From cefc2db8b7f7f65599869dadbcf1f62d56b2a3b2 Mon Sep 17 00:00:00 2001 From: Craig Moyer Date: Fri, 13 Jan 2017 19:55:41 -0500 Subject: [PATCH 1/8] Add support for folder base journal. Adds feature for issue #170 (and #398) where you configure your journal to be a directory and entries are added as sub-directories and files: yyyy/mm/dd.txt. Multiple entries in a day will go in the same file, but a new entry for a specific day will create a new file (and directory structure). --- features/folder.feature | 43 ++++++++++++++++ features/regression.feature | 6 --- features/steps/core.py | 9 ++++ jrnl/FolderJournal.py | 84 +++++++++++++++++++++++++++++++ jrnl/Journal.py | 4 +- jrnl/plugins/template_exporter.py | 2 +- 6 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 features/folder.feature create mode 100644 jrnl/FolderJournal.py diff --git a/features/folder.feature b/features/folder.feature new file mode 100644 index 00000000..b747e6c1 --- /dev/null +++ b/features/folder.feature @@ -0,0 +1,43 @@ +Feature: Testing a journal with a root directory and multiple files in the format of yyyy/mm/dd.txt + + Scenario: Opening an folder that's not a DayOne folder should treat as folder journal + Given we use the config "empty_folder.yaml" + When we run "jrnl 23 july 2013: Testing folder journal." + Then we should see the message "Entry added" + When we run "jrnl -1" + Then the output should be + """ + 2013-07-23 09:00 Testing folder journal. + """ + + Scenario: Adding entries to a Folder journal should generate date files + Given we use the config "empty_folder.yaml" + When we run "jrnl 23 July 2013: Testing folder journal." + Then we should see the message "Entry added" + When the journal directory is listed + Then the output should contain "2013/07/23.txt" + + + Scenario: Adding multiple entries to a Folder journal should generate multiple date files + Given we use the config "empty_folder.yaml" + When we run "jrnl 23 July 2013: Testing folder journal." + And we run "jrnl 3/7/2014: Second entry of journal." + Then we should see the message "Entry added" + When the journal directory is listed + Then the output should contain "2013/07/23.txt" + And the output should contain "2014/03/07.txt" + + + Scenario: Out of order entries to a Folder journal should be listed in date order + Given we use the config "empty_folder.yaml" + When we run "jrnl 3/7/2014 4:37pm: Second entry of journal." + Then we should see the message "Entry added" + When we run "jrnl 23 July 2013: Testing folder journal." + Then we should see the message "Entry added" + When we run "jrnl -2" + Then the output should be + """ + 2013-07-23 09:00 Testing folder journal. + + 2014-03-07 16:37 Second entry of journal. + """ diff --git a/features/regression.feature b/features/regression.feature index 727f7c27..aa78ae3b 100644 --- a/features/regression.feature +++ b/features/regression.feature @@ -8,12 +8,6 @@ Feature: Zapped bugs should stay dead. When we run "jrnl -n 1" Then the output should not contain "Life is good" - Scenario: Opening an folder that's not a DayOne folder gives a nice error message - Given we use the config "empty_folder.yaml" - When we run "jrnl Herro" - Then we should get an error - Then we should see the message "is a directory, but doesn't seem to be a DayOne journal either" - Scenario: Date with time should be parsed correctly # https://github.com/maebert/jrnl/issues/117 Given we use the config "basic.yaml" diff --git a/features/steps/core.py b/features/steps/core.py index 4813b252..4fabfcd9 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -254,6 +254,15 @@ def check_journal_entries(context, number, journal_name="default"): journal = open_journal(journal_name) assert len(journal.entries) == number +@when('the journal directory is listed') +def list_journal_directory(context, journal="default"): + files=[] + with open(install.CONFIG_FILE_PATH) as config_file: + config = yaml.load(config_file) + journal_path = config['journals'][journal] + for root, dirnames, f in os.walk(journal_path): + for file in f: + print(os.path.join(root,file)) @then('fail') def debug_fail(context): diff --git a/jrnl/FolderJournal.py b/jrnl/FolderJournal.py new file mode 100644 index 00000000..4ca55335 --- /dev/null +++ b/jrnl/FolderJournal.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# encoding: utf-8 + +from __future__ import absolute_import, unicode_literals +from . import Entry +from . import Journal +import codecs +import os +import fnmatch + +def get_files(journal_config): + """Searches through sub directories starting with journal_config and find all text files""" + filenames = [] + for root, dirnames, f in os.walk(journal_config): + for filename in fnmatch.filter(f, '*.txt'): + filenames.append(os.path.join(root, filename)) + return filenames + + +class Folder(Journal.Journal): + """A Journal handling multiple files in a folder""" + + def __init__(self, **kwargs): + self.entries = [] + self._diff_entry_dates = [] + super(Folder, self).__init__(**kwargs) + + + def open(self): + filenames = [] + self.entries = [] + filenames = get_files(self.config['journal']) + for filename in filenames: + with codecs.open(filename, "r", "utf-8") as f: + journal = f.read() + self.entries.extend(self._parse(journal)) + self.sort() + return self + + def write(self): + """Writes only the entries that have been modified into proper files.""" + #Create a list of dates of modified entries. Start with diff_entry_dates + modified_dates = self._diff_entry_dates + seen_dates = set(self._diff_entry_dates) + for e in self.entries: + if e.modified: + if e.date not in seen_dates: + modified_dates.append(e.date) + seen_dates.add(e.date) + + #For every date that had a modified entry, write to a file + for d in modified_dates: + write_entries=[] + filename = os.path.join(self.config['journal'], d.strftime("%Y"), d.strftime("%m"), d.strftime("%d")+".txt") + dirname = os.path.dirname(filename) + #create directory if it doesn't exist + if not os.path.exists(dirname): + os.makedirs(dirname) + for e in self.entries: + if e.date.year == d.year and e.date.month == d.month and e.date.day == d.day: + write_entries.append(e) + journal = "\n".join([e.__unicode__() for e in write_entries]) + with codecs.open(filename, 'w', "utf-8") as journal_file: + journal_file.write(journal) + #look for and delete empty files + filenames = [] + filenames = get_files(self.config['journal']) + for filename in filenames: + if os.stat(filename).st_size <= 0: + #print("empty file: {}".format(filename)) + os.remove(filename) + + def parse_editable_str(self, edited): + """Parses the output of self.editable_str and updates it's entries.""" + mod_entries = self._parse(edited) + diff_entries = set(self.entries) - set(mod_entries) + for e in diff_entries: + self._diff_entry_dates.append(e.date) + # Match those entries that can be found in self.entries and set + # these to modified, so we can get a count of how many entries got + # modified and how many got deleted later. + for entry in mod_entries: + entry.modified = not any(entry == old_entry for old_entry in self.entries) + self.entries = mod_entries diff --git a/jrnl/Journal.py b/jrnl/Journal.py index c36b3760..8eb1d2da 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -314,8 +314,8 @@ def open_journal(name, config, legacy=False): from . import DayOneJournal return DayOneJournal.DayOne(**config).open() else: - util.prompt(u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])) - sys.exit(1) + from . import FolderJournal + return FolderJournal.Folder(**config).open() if not config['encrypt']: if legacy: diff --git a/jrnl/plugins/template_exporter.py b/jrnl/plugins/template_exporter.py index 6a5fa86b..85aa2236 100644 --- a/jrnl/plugins/template_exporter.py +++ b/jrnl/plugins/template_exporter.py @@ -36,7 +36,7 @@ def __exporter_from_file(template_file): """Create a template class from a file""" name = os.path.basename(template_file).replace(".template", "") template = Template.from_file(template_file) - return type("{}Exporter".format(name.title()), (GenericTemplateExporter, ), { + return type(str("{}Exporter".format(name.title())), (GenericTemplateExporter, ), { "names": [name], "extension": template.extension, "template": template From 12282b858b0cce8accc4c5b730563835ac4b6c36 Mon Sep 17 00:00:00 2001 From: Craig Moyer Date: Fri, 13 Jan 2017 20:03:10 -0500 Subject: [PATCH 2/8] Update travis to remove --use-mirrors for 2.0-rc1 branch. Was causing errors in builds. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d2eea287..6a195a7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: - "3.3" - "3.4" install: - - "pip install -e . --use-mirrors" + - "pip install -e ." - "pip install -q behave" # command to run tests script: From 889b762247d36bdcd7860b94a4294304c9cde1de Mon Sep 17 00:00:00 2001 From: moyercw Date: Sat, 14 Jan 2017 01:26:22 +0000 Subject: [PATCH 3/8] Add missing dependency for asteval. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index e96973be..f6097064 100644 --- a/setup.py +++ b/setup.py @@ -89,6 +89,7 @@ setup( "keyring>=7.3", "passlib>=1.6.2", "pyxdg>=0.25", + "asteval>=0.9.8", ] + [p for p, cond in conditional_dependencies.items() if cond], long_description=__doc__, entry_points={ From 866634035a3ab46c2d187a817826e45295b5ebe2 Mon Sep 17 00:00:00 2001 From: Craig Moyer Date: Mon, 23 Sep 2019 17:20:07 -0400 Subject: [PATCH 4/8] Fix failed test scenarios for folder journal. --- features/data/journals/empty_folder/{empty.txt => empty} | 0 features/steps/core.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename features/data/journals/empty_folder/{empty.txt => empty} (100%) diff --git a/features/data/journals/empty_folder/empty.txt b/features/data/journals/empty_folder/empty similarity index 100% rename from features/data/journals/empty_folder/empty.txt rename to features/data/journals/empty_folder/empty diff --git a/features/steps/core.py b/features/steps/core.py index a6fcae16..4387fb39 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -258,7 +258,7 @@ def check_journal_entries(context, number, journal_name="default"): def list_journal_directory(context, journal="default"): files=[] with open(install.CONFIG_FILE_PATH) as config_file: - config = yaml.load(config_file) + config = yaml.load(config_file, Loader=yaml.FullLoader) journal_path = config['journals'][journal] for root, dirnames, f in os.walk(journal_path): for file in f: From 53c557005c00f5d0fa235baa66c50cf1afccab52 Mon Sep 17 00:00:00 2001 From: Micah Ellison <4383304+micahellison@users.noreply.github.com> Date: Sat, 29 Feb 2020 12:35:05 -0800 Subject: [PATCH 5/8] Update FolderJournal reference to entry __unicode__ method to __str__ --- jrnl/FolderJournal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jrnl/FolderJournal.py b/jrnl/FolderJournal.py index 4ca55335..ac3dabd8 100644 --- a/jrnl/FolderJournal.py +++ b/jrnl/FolderJournal.py @@ -59,7 +59,7 @@ class Folder(Journal.Journal): for e in self.entries: if e.date.year == d.year and e.date.month == d.month and e.date.day == d.day: write_entries.append(e) - journal = "\n".join([e.__unicode__() for e in write_entries]) + journal = "\n".join([e.__str__() for e in write_entries]) with codecs.open(filename, 'w', "utf-8") as journal_file: journal_file.write(journal) #look for and delete empty files From 71f918d87827d790327a6f62805730d39952a444 Mon Sep 17 00:00:00 2001 From: Micah Ellison <4383304+micahellison@users.noreply.github.com> Date: Sat, 29 Feb 2020 12:45:47 -0800 Subject: [PATCH 6/8] Remove DayOne test made obsolete by FolderJournal --- features/dayone_regressions.feature | 6 ------ 1 file changed, 6 deletions(-) diff --git a/features/dayone_regressions.feature b/features/dayone_regressions.feature index 62c8cc24..3e98f9e9 100644 --- a/features/dayone_regressions.feature +++ b/features/dayone_regressions.feature @@ -23,9 +23,3 @@ Feature: Zapped Dayone bugs stay dead! 2014-04-24 09:00 Ran 6.2 miles today in 1:02:03. | I'm feeling sore because I forgot to stretch. """ - - Scenario: Opening an folder that's not a DayOne folder gives a nice error message - Given we use the config "empty_folder.yaml" - When we run "jrnl Herro" - Then we should get an error - Then we should see the message "is a directory, but doesn't seem to be a DayOne journal either" From 8a4fe0ec81760db06e119fd374fd74d0e4837985 Mon Sep 17 00:00:00 2001 From: Micah Ellison <4383304+micahellison@users.noreply.github.com> Date: Sat, 29 Feb 2020 13:29:19 -0800 Subject: [PATCH 7/8] Fix FolderJournal path tests for Windows paths --- features/folder.feature | 7 +++---- features/steps/core.py | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/features/folder.feature b/features/folder.feature index b747e6c1..650ac53e 100644 --- a/features/folder.feature +++ b/features/folder.feature @@ -15,7 +15,7 @@ Feature: Testing a journal with a root directory and multiple files in the forma When we run "jrnl 23 July 2013: Testing folder journal." Then we should see the message "Entry added" When the journal directory is listed - Then the output should contain "2013/07/23.txt" + Then the output should contain "2013/07/23.txt" or "2013\07\23.txt" Scenario: Adding multiple entries to a Folder journal should generate multiple date files @@ -24,9 +24,8 @@ Feature: Testing a journal with a root directory and multiple files in the forma And we run "jrnl 3/7/2014: Second entry of journal." Then we should see the message "Entry added" When the journal directory is listed - Then the output should contain "2013/07/23.txt" - And the output should contain "2014/03/07.txt" - + Then the output should contain "2013/07/23.txt" or "2013\07\23.txt" + Then the output should contain "2014/03/07.txt" or "2014\03\07.txt" Scenario: Out of order entries to a Folder journal should be listed in date order Given we use the config "empty_folder.yaml" diff --git a/features/steps/core.py b/features/steps/core.py index 6ada7b2f..98822813 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -210,10 +210,11 @@ def check_output_time_inline(context, text): @then("the output should contain") @then('the output should contain "{text}"') -def check_output_inline(context, text=None): +@then('the output should contain "{text}" or "{text2}"') +def check_output_inline(context, text=None, text2=None): text = text or context.text out = context.stdout_capture.getvalue() - assert text in out, text + assert (text in out or text2 in out), text or text2 @then('the output should not contain "{text}"') From da30eaa62ffeef05ae0899dbc61b095889eafff3 Mon Sep 17 00:00:00 2001 From: Micah Ellison <4383304+micahellison@users.noreply.github.com> Date: Sat, 29 Feb 2020 14:03:54 -0800 Subject: [PATCH 8/8] Apply Black linting --- features/steps/core.py | 14 ++++++++------ jrnl/EncryptedJournal.py | 2 +- jrnl/FolderJournal.py | 35 ++++++++++++++++++++++------------- jrnl/Journal.py | 1 + 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/features/steps/core.py b/features/steps/core.py index 98822813..82cae225 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -214,7 +214,7 @@ def check_output_time_inline(context, text): def check_output_inline(context, text=None, text2=None): text = text or context.text out = context.stdout_capture.getvalue() - assert (text in out or text2 in out), text or text2 + assert text in out or text2 in out, text or text2 @then('the output should not contain "{text}"') @@ -270,15 +270,17 @@ def check_journal_entries(context, number, journal_name="default"): journal = open_journal(journal_name) assert len(journal.entries) == number -@when('the journal directory is listed') + +@when("the journal directory is listed") def list_journal_directory(context, journal="default"): - files=[] + files = [] with open(install.CONFIG_FILE_PATH) as config_file: config = yaml.load(config_file, Loader=yaml.FullLoader) - journal_path = config['journals'][journal] + journal_path = config["journals"][journal] for root, dirnames, f in os.walk(journal_path): - for file in f: - print(os.path.join(root,file)) + for file in f: + print(os.path.join(root, file)) + @then("fail") def debug_fail(context): diff --git a/jrnl/EncryptedJournal.py b/jrnl/EncryptedJournal.py index d6681a47..cc5af748 100644 --- a/jrnl/EncryptedJournal.py +++ b/jrnl/EncryptedJournal.py @@ -23,7 +23,7 @@ def make_key(password): length=32, # Salt is hard-coded salt=b"\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8", - iterations=100000, + iterations=100_000, backend=default_backend(), ) key = kdf.derive(password) diff --git a/jrnl/FolderJournal.py b/jrnl/FolderJournal.py index ac3dabd8..19519a14 100644 --- a/jrnl/FolderJournal.py +++ b/jrnl/FolderJournal.py @@ -8,11 +8,12 @@ import codecs import os import fnmatch + def get_files(journal_config): """Searches through sub directories starting with journal_config and find all text files""" filenames = [] for root, dirnames, f in os.walk(journal_config): - for filename in fnmatch.filter(f, '*.txt'): + for filename in fnmatch.filter(f, "*.txt"): filenames.append(os.path.join(root, filename)) return filenames @@ -25,11 +26,10 @@ class Folder(Journal.Journal): self._diff_entry_dates = [] super(Folder, self).__init__(**kwargs) - def open(self): filenames = [] self.entries = [] - filenames = get_files(self.config['journal']) + filenames = get_files(self.config["journal"]) for filename in filenames: with codecs.open(filename, "r", "utf-8") as f: journal = f.read() @@ -39,7 +39,7 @@ class Folder(Journal.Journal): def write(self): """Writes only the entries that have been modified into proper files.""" - #Create a list of dates of modified entries. Start with diff_entry_dates + # Create a list of dates of modified entries. Start with diff_entry_dates modified_dates = self._diff_entry_dates seen_dates = set(self._diff_entry_dates) for e in self.entries: @@ -48,26 +48,35 @@ class Folder(Journal.Journal): modified_dates.append(e.date) seen_dates.add(e.date) - #For every date that had a modified entry, write to a file + # For every date that had a modified entry, write to a file for d in modified_dates: - write_entries=[] - filename = os.path.join(self.config['journal'], d.strftime("%Y"), d.strftime("%m"), d.strftime("%d")+".txt") + write_entries = [] + filename = os.path.join( + self.config["journal"], + d.strftime("%Y"), + d.strftime("%m"), + d.strftime("%d") + ".txt", + ) dirname = os.path.dirname(filename) - #create directory if it doesn't exist + # create directory if it doesn't exist if not os.path.exists(dirname): os.makedirs(dirname) for e in self.entries: - if e.date.year == d.year and e.date.month == d.month and e.date.day == d.day: + if ( + e.date.year == d.year + and e.date.month == d.month + and e.date.day == d.day + ): write_entries.append(e) journal = "\n".join([e.__str__() for e in write_entries]) - with codecs.open(filename, 'w', "utf-8") as journal_file: + with codecs.open(filename, "w", "utf-8") as journal_file: journal_file.write(journal) - #look for and delete empty files + # look for and delete empty files filenames = [] - filenames = get_files(self.config['journal']) + filenames = get_files(self.config["journal"]) for filename in filenames: if os.stat(filename).st_size <= 0: - #print("empty file: {}".format(filename)) + # print("empty file: {}".format(filename)) os.remove(filename) def parse_editable_str(self, edited): diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 2310579a..9d868807 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -379,6 +379,7 @@ def open_journal(name, config, legacy=False): return DayOneJournal.DayOne(**config).open() else: from . import FolderJournal + return FolderJournal.Folder(**config).open() if not config["encrypt"]: