mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 16:48:31 +02:00
Allow editing of DayOne entries (#1001)
* add test to repro issue #955 * Allow editing of DayOne entries * Add broken test for Dayone Add test for editing Dayone entries (this test currently fails) Co-authored-by: Jonathan Wren <jonathan@nowandwren.com> * Fix editing logic for DayOneJournal DayOneJournal previously reimplemented Journal._parse inside of DayOneJournal.parse_editable_string, and in doing so caused issues between itself and the class it was inheriting from. This commit fixes the issue by moving the UUID to be in the body of the entry, rather than above it. So, then Journal._parse still finds the correct boundaries between entries, and DayOneJournal then parses the UUID afterward. Co-authored-by: MinchinWeb <w_minchin@hotmail.com> Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com>
This commit is contained in:
parent
4c2f2861db
commit
a11aa24c7e
5 changed files with 85 additions and 82 deletions
|
@ -1,6 +1,6 @@
|
||||||
default_hour: 9
|
default_hour: 9
|
||||||
default_minute: 0
|
default_minute: 0
|
||||||
editor: ''
|
editor: noop
|
||||||
template: false
|
template: false
|
||||||
encrypt: false
|
encrypt: false
|
||||||
highlight: true
|
highlight: true
|
||||||
|
|
|
@ -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.generation_date
|
||||||
and the json output should contain entries.0.creator.device_agent
|
and the json output should contain entries.0.creator.device_agent
|
||||||
and "entries.0.creator.software_agent" in the json output should contain "jrnl"
|
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"
|
||||||
|
|
|
@ -101,15 +101,24 @@ def move_up_dir(context, path):
|
||||||
os.chdir(path)
|
os.chdir(path)
|
||||||
|
|
||||||
|
|
||||||
@when('we open the editor and enter "{text}"')
|
@when("we open the editor and {method}")
|
||||||
@when("we open the editor and enter nothing")
|
@when('we open the editor and {method} "{text}"')
|
||||||
def open_editor_and_enter(context, 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 ""
|
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):
|
def _mock_editor_function(command):
|
||||||
context.editor_command = command
|
context.editor_command = command
|
||||||
tmpfile = command[-1]
|
tmpfile = command[-1]
|
||||||
with open(tmpfile, "w+") as f:
|
with open(tmpfile, file_method) as f:
|
||||||
f.write(text)
|
f.write(text)
|
||||||
|
|
||||||
return tmpfile
|
return tmpfile
|
||||||
|
@ -120,7 +129,7 @@ def open_editor_and_enter(context, text=""):
|
||||||
patch("subprocess.call", side_effect=_mock_editor_function), \
|
patch("subprocess.call", side_effect=_mock_editor_function), \
|
||||||
patch("sys.stdin.isatty", return_value=True) \
|
patch("sys.stdin.isatty", return_value=True) \
|
||||||
:
|
:
|
||||||
context.execute_steps('when we run "jrnl"')
|
cli.run(["--edit"])
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
|
@ -209,8 +218,13 @@ def run(context, command, cache_dir=None):
|
||||||
|
|
||||||
args = ushlex(command)
|
args = ushlex(command)
|
||||||
|
|
||||||
|
def _mock_editor(command):
|
||||||
|
context.editor_command = command
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with patch("sys.argv", args):
|
with patch("sys.argv", args), patch(
|
||||||
|
"subprocess.call", side_effect=_mock_editor
|
||||||
|
):
|
||||||
cli.run(args[1:])
|
cli.run(args[1:])
|
||||||
context.exit_status = 0
|
context.exit_status = 0
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
|
@ -282,7 +296,7 @@ def check_output_version_inline(context):
|
||||||
def check_output_inline(context, text=None, text2=None):
|
def check_output_inline(context, text=None, text2=None):
|
||||||
text = text or context.text
|
text = text or context.text
|
||||||
out = context.stdout_capture.getvalue()
|
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")
|
@then("the error output should contain")
|
||||||
|
|
|
@ -16,7 +16,6 @@ import pytz
|
||||||
import tzlocal
|
import tzlocal
|
||||||
|
|
||||||
from . import __title__, __version__, Entry, Journal
|
from . import __title__, __version__, Entry, Journal
|
||||||
from . import time as jrnl_time
|
|
||||||
|
|
||||||
|
|
||||||
class DayOne(Journal.Journal):
|
class DayOne(Journal.Journal):
|
||||||
|
@ -179,7 +178,26 @@ class DayOne(Journal.Journal):
|
||||||
def editable_str(self):
|
def editable_str(self):
|
||||||
"""Turns the journal into a string of entries that can be edited
|
"""Turns the journal into a string of entries that can be edited
|
||||||
manually and later be parsed with eslf.parse_editable_str."""
|
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):
|
def parse_editable_str(self, edited):
|
||||||
"""Parses the output of self.editable_str and updates its entries."""
|
"""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
|
# UUIDs of the new entries against self.entries, updating the entries
|
||||||
# if the edited entries differ, and deleting entries from self.entries
|
# if the edited entries differ, and deleting entries from self.entries
|
||||||
# if they don't show up in the edited entries anymore.
|
# if they don't show up in the edited entries anymore.
|
||||||
|
entries_from_editor = self._parse(edited)
|
||||||
|
|
||||||
# Initialise our current entry
|
for entry in entries_from_editor:
|
||||||
entries = []
|
entry = self._get_and_remove_uuid_from_entry(entry)
|
||||||
current_entry = None
|
|
||||||
|
|
||||||
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
|
# 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._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]
|
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
|
||||||
|
|
|
@ -13,7 +13,9 @@ class Entry:
|
||||||
self.journal = journal # Reference to journal mainly to access its config
|
self.journal = journal # Reference to journal mainly to access its config
|
||||||
self.date = date or datetime.now()
|
self.date = date or datetime.now()
|
||||||
self.text = text
|
self.text = text
|
||||||
self._title = self._body = self._tags = None
|
self._title = None
|
||||||
|
self._body = None
|
||||||
|
self._tags = None
|
||||||
self.starred = starred
|
self.starred = starred
|
||||||
self.modified = False
|
self.modified = False
|
||||||
|
|
||||||
|
@ -37,18 +39,30 @@ class Entry:
|
||||||
self._parse_text()
|
self._parse_text()
|
||||||
return self._title
|
return self._title
|
||||||
|
|
||||||
|
@title.setter
|
||||||
|
def title(self, x):
|
||||||
|
self._title = x
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def body(self):
|
def body(self):
|
||||||
if self._body is None:
|
if self._body is None:
|
||||||
self._parse_text()
|
self._parse_text()
|
||||||
return self._body
|
return self._body
|
||||||
|
|
||||||
|
@body.setter
|
||||||
|
def body(self, x):
|
||||||
|
self._body = x
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tags(self):
|
def tags(self):
|
||||||
if self._tags is None:
|
if self._tags is None:
|
||||||
self._parse_text()
|
self._parse_text()
|
||||||
return self._tags
|
return self._tags
|
||||||
|
|
||||||
|
@tags.setter
|
||||||
|
def tags(self, x):
|
||||||
|
self._tags = x
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def tag_regex(tagsymbols):
|
def tag_regex(tagsymbols):
|
||||||
pattern = fr"(?<!\S)([{tagsymbols}][-+*#/\w]+)"
|
pattern = fr"(?<!\S)([{tagsymbols}][-+*#/\w]+)"
|
||||||
|
|
Loading…
Add table
Reference in a new issue