From cefc2db8b7f7f65599869dadbcf1f62d56b2a3b2 Mon Sep 17 00:00:00 2001 From: Craig Moyer Date: Fri, 13 Jan 2017 19:55:41 -0500 Subject: [PATCH] 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