Skip to content

Commit

Permalink
Allow editing of DayOne entries (#1001)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people committed Jul 18, 2020
1 parent 4c2f286 commit a11aa24
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 82 deletions.
2 changes: 1 addition & 1 deletion features/data/configs/dayone.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
default_hour: 9
default_minute: 0
editor: ''
editor: noop
template: false
encrypt: false
highlight: true
Expand Down
18 changes: 18 additions & 0 deletions features/dayone.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
28 changes: 21 additions & 7 deletions features/steps/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -120,7 +129,7 @@ def _mock_editor_function(command):
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


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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")
Expand Down
103 changes: 30 additions & 73 deletions jrnl/DayOneJournal.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import tzlocal

from . import __title__, __version__, Entry, Journal
from . import time as jrnl_time


class DayOne(Journal.Journal):
Expand Down Expand Up @@ -179,87 +178,45 @@ def write(self):
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."""
# Method: create a new list of entries from the edited text, then match
# 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 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)
for entry in entries_from_editor:
entry = self._get_and_remove_uuid_from_entry(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
16 changes: 15 additions & 1 deletion jrnl/Entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ def __init__(self, journal, date=None, text="", starred=False):
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

Expand All @@ -37,18 +39,30 @@ def title(self):
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"(?<!\S)([{tagsymbols}][-+*#/\w]+)"
Expand Down

0 comments on commit a11aa24

Please sign in to comment.