diff --git a/features/data/configs/dayone.yaml b/features/data/configs/dayone.yaml index 8807e66b..894cb911 100644 --- a/features/data/configs/dayone.yaml +++ b/features/data/configs/dayone.yaml @@ -1,6 +1,6 @@ default_hour: 9 default_minute: 0 -editor: '' +editor: noop template: false encrypt: false highlight: true diff --git a/features/dayone.feature b/features/dayone.feature index bf74dc8d..39abae95 100644 --- a/features/dayone.feature +++ b/features/dayone.feature @@ -75,3 +75,21 @@ Feature: Dayone specific implementation details. and the json output should contain entries.0.creator.generation_date and the json output should contain entries.0.creator.device_agent and "entries.0.creator.software_agent" in the json output should contain "jrnl" + + Scenario: Editing Dayone with mock editor + Given we use the config "dayone.yaml" + When we run "jrnl --edit" + Then we should get no error + + Scenario: Editing Dayone entries + Given we use the config "dayone.yaml" + When we open the editor and append + """ + Here is the first line. + Here is the second line. + """ + When we run "jrnl -n 1" + Then we should get no error + and the output should contain "This entry is starred!" + and the output should contain "Here is the first line" + and the output should contain "Here is the second line" diff --git a/features/steps/core.py b/features/steps/core.py index 5cf5b79b..9c8fa8e6 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -101,15 +101,24 @@ def move_up_dir(context, path): os.chdir(path) -@when('we open the editor and enter "{text}"') -@when("we open the editor and enter nothing") -def open_editor_and_enter(context, text=""): +@when("we open the editor and {method}") +@when('we open the editor and {method} "{text}"') +@when("we open the editor and {method} nothing") +@when("we open the editor and {method} nothing") +def open_editor_and_enter(context, method, text=""): text = text or context.text or "" + if method == "enter": + file_method = "w+" + elif method == "append": + file_method = "a" + else: + file_method = "r+" + def _mock_editor_function(command): context.editor_command = command tmpfile = command[-1] - with open(tmpfile, "w+") as f: + with open(tmpfile, file_method) as f: f.write(text) return tmpfile @@ -120,7 +129,7 @@ def open_editor_and_enter(context, text=""): patch("subprocess.call", side_effect=_mock_editor_function), \ patch("sys.stdin.isatty", return_value=True) \ : - context.execute_steps('when we run "jrnl"') + cli.run(["--edit"]) # fmt: on @@ -209,8 +218,13 @@ def run(context, command, cache_dir=None): args = ushlex(command) + def _mock_editor(command): + context.editor_command = command + try: - with patch("sys.argv", args): + with patch("sys.argv", args), patch( + "subprocess.call", side_effect=_mock_editor + ): cli.run(args[1:]) context.exit_status = 0 except SystemExit as e: @@ -282,7 +296,7 @@ def check_output_version_inline(context): 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 and text in out) or (text2 and text2 in out) @then("the error output should contain") diff --git a/jrnl/DayOneJournal.py b/jrnl/DayOneJournal.py index dd1cbc24..cb568bc3 100644 --- a/jrnl/DayOneJournal.py +++ b/jrnl/DayOneJournal.py @@ -16,7 +16,6 @@ import pytz import tzlocal from . import __title__, __version__, Entry, Journal -from . import time as jrnl_time class DayOne(Journal.Journal): @@ -179,7 +178,26 @@ class DayOne(Journal.Journal): def editable_str(self): """Turns the journal into a string of entries that can be edited manually and later be parsed with eslf.parse_editable_str.""" - return "\n".join([f"# {e.uuid}\n{str(e)}" for e in self.entries]) + return "\n".join([f"{str(e)}\n# {e.uuid}\n" for e in self.entries]) + + def _update_old_entry(self, entry, new_entry): + for attr in ("title", "body", "date"): + old_attr = getattr(entry, attr) + new_attr = getattr(new_entry, attr) + if old_attr != new_attr: + entry.modified = True + setattr(entry, attr, new_attr) + + def _get_and_remove_uuid_from_entry(self, entry): + uuid_regex = "^ *?# ([a-zA-Z0-9]+) *?$" + m = re.search(uuid_regex, entry.body, re.MULTILINE) + entry.uuid = m.group(1) if m else None + + # remove the uuid from the body + entry.body = re.sub(uuid_regex, "", entry.body, flags=re.MULTILINE, count=1) + entry.body = entry.body.rstrip() + + return entry def parse_editable_str(self, edited): """Parses the output of self.editable_str and updates its entries.""" @@ -187,79 +205,18 @@ class DayOne(Journal.Journal): # UUIDs of the new entries against self.entries, updating the entries # if the edited entries differ, and deleting entries from self.entries # if they don't show up in the edited entries anymore. + entries_from_editor = self._parse(edited) - # Initialise our current entry - entries = [] - current_entry = None + for entry in entries_from_editor: + entry = self._get_and_remove_uuid_from_entry(entry) - for line in edited.splitlines(): - # try to parse line as UUID => new entry begins - line = line.rstrip() - m = re.match("# *([a-f0-9]+) *$", line.lower()) - if m: - if current_entry: - entries.append(current_entry) - current_entry = Entry.Entry(self) - current_entry.modified = False - current_entry.uuid = m.group(1).lower() - else: - date_blob_re = re.compile("^\\[[^\\]]+\\] ") - date_blob = date_blob_re.findall(line) - if date_blob: - date_blob = date_blob[0] - new_date = jrnl_time.parse(date_blob.strip(" []")) - if line.endswith("*"): - current_entry.starred = True - line = line[:-1] - current_entry.title = line[len(date_blob) - 1 :].strip() - current_entry.date = new_date - elif current_entry: - current_entry.body += line + "\n" - - # Append last entry - if current_entry: - entries.append(current_entry) - - # Now, update our current entries if they changed - for entry in entries: - entry._parse_text() - matched_entries = [ - e for e in self.entries if e.uuid.lower() == entry.uuid.lower() - ] - # tags in entry body - if matched_entries: - # This entry is an existing entry - match = matched_entries[0] - - # merge existing tags with tags pulled from the entry body - entry.tags = list(set(entry.tags + match.tags)) - - # extended Dayone metadata - if hasattr(match, "creator_device_agent"): - entry.creator_device_agent = match.creator_device_agent - if hasattr(match, "creator_generation_date"): - entry.creator_generation_date = match.creator_generation_date - if hasattr(match, "creator_host_name"): - entry.creator_host_name = match.creator_host_name - if hasattr(match, "creator_os_agent"): - entry.creator_os_agent = match.creator_os_agent - if hasattr(match, "creator_software_agent"): - entry.creator_software_agent = match.creator_software_agent - if hasattr(match, "location"): - entry.location = match.location - if hasattr(match, "weather"): - entry.weather = match.weather - - if match != entry: - self.entries.remove(match) - entry.modified = True - self.entries.append(entry) - else: - # This entry seems to be new... save it. - entry.modified = True - self.entries.append(entry) # Remove deleted entries - edited_uuids = [e.uuid for e in entries] + edited_uuids = [e.uuid for e in entries_from_editor] self._deleted_entries = [e for e in self.entries if e.uuid not in edited_uuids] self.entries[:] = [e for e in self.entries if e.uuid in edited_uuids] - return entries + + for entry in entries_from_editor: + for old_entry in self.entries: + if entry.uuid == old_entry.uuid: + self._update_old_entry(old_entry, entry) + break diff --git a/jrnl/Entry.py b/jrnl/Entry.py index 1197e2f8..807ed86d 100755 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -13,7 +13,9 @@ class Entry: self.journal = journal # Reference to journal mainly to access its config self.date = date or datetime.now() self.text = text - self._title = self._body = self._tags = None + self._title = None + self._body = None + self._tags = None self.starred = starred self.modified = False @@ -37,18 +39,30 @@ class Entry: self._parse_text() return self._title + @title.setter + def title(self, x): + self._title = x + @property def body(self): if self._body is None: self._parse_text() return self._body + @body.setter + def body(self, x): + self._body = x + @property def tags(self): if self._tags is None: self._parse_text() return self._tags + @tags.setter + def tags(self, x): + self._tags = x + @staticmethod def tag_regex(tagsymbols): pattern = fr"(?