mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 16:48:31 +02:00
Add test scenarios for the export feature (#824)
* Fix behave keyword "and" to correct case "And" * Extract export_steps module * Add scenario for XML export * Add scenario for tags export * Add scenario for fancy export * Add scenario for yaml export * Remove unused module export.py * Run `make format` * Fix `create_directory` step
This commit is contained in:
parent
aacc245dc8
commit
8b7ebe2dcd
4 changed files with 187 additions and 120 deletions
|
@ -4,21 +4,20 @@ Feature: Exporting a Journal
|
||||||
Given we use the config "tags.yaml"
|
Given we use the config "tags.yaml"
|
||||||
When we run "jrnl --export json"
|
When we run "jrnl --export json"
|
||||||
Then we should get no error
|
Then we should get no error
|
||||||
and the output should be parsable as json
|
And the output should be parsable as json
|
||||||
and "entries" in the json output should have 2 elements
|
And "entries" in the json output should have 2 elements
|
||||||
and "tags" in the json output should contain "@idea"
|
And "tags" in the json output should contain "@idea"
|
||||||
and "tags" in the json output should contain "@journal"
|
And "tags" in the json output should contain "@journal"
|
||||||
and "tags" in the json output should contain "@dan"
|
And "tags" in the json output should contain "@dan"
|
||||||
|
|
||||||
Scenario: Exporting using filters should only export parts of the journal
|
Scenario: Exporting using filters should only export parts of the journal
|
||||||
Given we use the config "tags.yaml"
|
Given we use the config "tags.yaml"
|
||||||
When we run "jrnl -until 'may 2013' --export json"
|
When we run "jrnl -until 'may 2013' --export json"
|
||||||
# Then we should get no error
|
|
||||||
Then the output should be parsable as json
|
Then the output should be parsable as json
|
||||||
and "entries" in the json output should have 1 element
|
And "entries" in the json output should have 1 element
|
||||||
and "tags" in the json output should contain "@idea"
|
And "tags" in the json output should contain "@idea"
|
||||||
and "tags" in the json output should contain "@journal"
|
And "tags" in the json output should contain "@journal"
|
||||||
and "tags" in the json output should not contain "@dan"
|
And "tags" in the json output should not contain "@dan"
|
||||||
|
|
||||||
Scenario: Exporting using custom templates
|
Scenario: Exporting using custom templates
|
||||||
Given we use the config "basic.yaml"
|
Given we use the config "basic.yaml"
|
||||||
|
@ -83,3 +82,57 @@ Feature: Exporting a Journal
|
||||||
More stuff
|
More stuff
|
||||||
more stuff again
|
more stuff again
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
Scenario: Exporting to XML
|
||||||
|
Given we use the config "tags.yaml"
|
||||||
|
When we run "jrnl --export xml"
|
||||||
|
Then the output should be a valid XML string
|
||||||
|
And "entries" node in the xml output should have 2 elements
|
||||||
|
And "tags" in the xml output should contain ["@idea", "@journal", "@dan"]
|
||||||
|
|
||||||
|
Scenario: Exporting tags
|
||||||
|
Given we use the config "tags.yaml"
|
||||||
|
When we run "jrnl --export tags"
|
||||||
|
Then the output should be
|
||||||
|
"""
|
||||||
|
@idea : 2
|
||||||
|
@journal : 1
|
||||||
|
@dan : 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Exporting fancy
|
||||||
|
Given we use the config "tags.yaml"
|
||||||
|
When we run "jrnl --export fancy"
|
||||||
|
Then the output should be
|
||||||
|
"""
|
||||||
|
┎──────────────────────────────────────────────────────────────╮2013-04-09 15:39
|
||||||
|
┃ I have an @idea: ╘═══════════════╕
|
||||||
|
┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||||
|
┃ (1) write a command line @journal software │
|
||||||
|
┃ (2) ??? │
|
||||||
|
┃ (3) PROFIT! │
|
||||||
|
┖──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┎──────────────────────────────────────────────────────────────╮2013-06-10 15:40
|
||||||
|
┃ I met with @dan. ╘═══════════════╕
|
||||||
|
┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||||
|
┃ As alway's he shared his latest @idea on how to rule the world with me. │
|
||||||
|
┃ inst │
|
||||||
|
┖──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Export to yaml
|
||||||
|
Given we use the config "tags.yaml"
|
||||||
|
And we created a directory named "exported_journal"
|
||||||
|
When we run "jrnl --export yaml -o exported_journal"
|
||||||
|
Then "exported_journal" should contain the files ["2013-04-09_i-have-an-idea.md", "2013-06-10_i-met-with-dan.md"]
|
||||||
|
And the content of exported yaml "exported_journal/2013-04-09_i-have-an-idea.md" should be
|
||||||
|
"""
|
||||||
|
title: I have an @idea:
|
||||||
|
date: 2013-04-09 15:39
|
||||||
|
stared: False
|
||||||
|
tags: idea, journal
|
||||||
|
|
||||||
|
(1) write a command line @journal software
|
||||||
|
(2) ???
|
||||||
|
(3) PROFIT!
|
||||||
|
"""
|
||||||
|
|
|
@ -3,7 +3,6 @@ from unittest.mock import patch
|
||||||
from behave import given, when, then
|
from behave import given, when, then
|
||||||
from jrnl import cli, install, Journal, util, plugins
|
from jrnl import cli, install, Journal, util, plugins
|
||||||
from jrnl import __version__
|
from jrnl import __version__
|
||||||
from dateutil import parser as date_parser
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -185,53 +184,6 @@ def no_error(context):
|
||||||
assert context.exit_status == 0, context.exit_status
|
assert context.exit_status == 0, context.exit_status
|
||||||
|
|
||||||
|
|
||||||
@then("the output should be parsable as json")
|
|
||||||
def check_output_json(context):
|
|
||||||
out = context.stdout_capture.getvalue()
|
|
||||||
assert json.loads(out), out
|
|
||||||
|
|
||||||
|
|
||||||
@then('"{field}" in the json output should have {number:d} elements')
|
|
||||||
@then('"{field}" in the json output should have 1 element')
|
|
||||||
def check_output_field(context, field, number=1):
|
|
||||||
out = context.stdout_capture.getvalue()
|
|
||||||
out_json = json.loads(out)
|
|
||||||
assert field in out_json, [field, out_json]
|
|
||||||
assert len(out_json[field]) == number, len(out_json[field])
|
|
||||||
|
|
||||||
|
|
||||||
@then('"{field}" in the json output should not contain "{key}"')
|
|
||||||
def check_output_field_not_key(context, field, key):
|
|
||||||
out = context.stdout_capture.getvalue()
|
|
||||||
out_json = json.loads(out)
|
|
||||||
assert field in out_json
|
|
||||||
assert key not in out_json[field]
|
|
||||||
|
|
||||||
|
|
||||||
@then('"{field}" in the json output should contain "{key}"')
|
|
||||||
def check_output_field_key(context, field, key):
|
|
||||||
out = context.stdout_capture.getvalue()
|
|
||||||
out_json = json.loads(out)
|
|
||||||
assert field in out_json
|
|
||||||
assert key in out_json[field]
|
|
||||||
|
|
||||||
|
|
||||||
@then('the json output should contain {path} = "{value}"')
|
|
||||||
def check_json_output_path(context, path, value):
|
|
||||||
""" E.g.
|
|
||||||
the json output should contain entries.0.title = "hello"
|
|
||||||
"""
|
|
||||||
out = context.stdout_capture.getvalue()
|
|
||||||
struct = json.loads(out)
|
|
||||||
|
|
||||||
for node in path.split("."):
|
|
||||||
try:
|
|
||||||
struct = struct[int(node)]
|
|
||||||
except ValueError:
|
|
||||||
struct = struct[node]
|
|
||||||
assert struct == value, struct
|
|
||||||
|
|
||||||
|
|
||||||
@then("the output should be")
|
@then("the output should be")
|
||||||
@then('the output should be "{text}"')
|
@then('the output should be "{text}"')
|
||||||
def check_output(context, text=None):
|
def check_output(context, text=None):
|
||||||
|
|
124
features/steps/export_steps.py
Normal file
124
features/steps/export_steps.py
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
|
from behave import then, given
|
||||||
|
|
||||||
|
|
||||||
|
@then("the output should be parsable as json")
|
||||||
|
def check_output_json(context):
|
||||||
|
out = context.stdout_capture.getvalue()
|
||||||
|
assert json.loads(out), out
|
||||||
|
|
||||||
|
|
||||||
|
@then('"{field}" in the json output should have {number:d} elements')
|
||||||
|
@then('"{field}" in the json output should have 1 element')
|
||||||
|
def check_output_field(context, field, number=1):
|
||||||
|
out = context.stdout_capture.getvalue()
|
||||||
|
out_json = json.loads(out)
|
||||||
|
assert field in out_json, [field, out_json]
|
||||||
|
assert len(out_json[field]) == number, len(out_json[field])
|
||||||
|
|
||||||
|
|
||||||
|
@then('"{field}" in the json output should not contain "{key}"')
|
||||||
|
def check_output_field_not_key(context, field, key):
|
||||||
|
out = context.stdout_capture.getvalue()
|
||||||
|
out_json = json.loads(out)
|
||||||
|
assert field in out_json
|
||||||
|
assert key not in out_json[field]
|
||||||
|
|
||||||
|
|
||||||
|
@then('"{field}" in the json output should contain "{key}"')
|
||||||
|
def check_output_field_key(context, field, key):
|
||||||
|
out = context.stdout_capture.getvalue()
|
||||||
|
out_json = json.loads(out)
|
||||||
|
assert field in out_json
|
||||||
|
assert key in out_json[field]
|
||||||
|
|
||||||
|
|
||||||
|
@then('the json output should contain {path} = "{value}"')
|
||||||
|
def check_json_output_path(context, path, value):
|
||||||
|
""" E.g.
|
||||||
|
the json output should contain entries.0.title = "hello"
|
||||||
|
"""
|
||||||
|
out = context.stdout_capture.getvalue()
|
||||||
|
struct = json.loads(out)
|
||||||
|
|
||||||
|
for node in path.split("."):
|
||||||
|
try:
|
||||||
|
struct = struct[int(node)]
|
||||||
|
except ValueError:
|
||||||
|
struct = struct[node]
|
||||||
|
assert struct == value, struct
|
||||||
|
|
||||||
|
|
||||||
|
@then("the output should be a valid XML string")
|
||||||
|
def assert_valid_xml_string(context):
|
||||||
|
output = context.stdout_capture.getvalue()
|
||||||
|
xml_tree = ElementTree.fromstring(output)
|
||||||
|
assert xml_tree, output
|
||||||
|
|
||||||
|
|
||||||
|
@then('"entries" node in the xml output should have {number:d} elements')
|
||||||
|
def assert_xml_output_entries_count(context, number):
|
||||||
|
output = context.stdout_capture.getvalue()
|
||||||
|
xml_tree = ElementTree.fromstring(output)
|
||||||
|
|
||||||
|
xml_tags = (node.tag for node in xml_tree)
|
||||||
|
assert "entries" in xml_tags, str(list(xml_tags))
|
||||||
|
|
||||||
|
actual_entry_count = len(xml_tree.find("entries"))
|
||||||
|
assert actual_entry_count == number, actual_entry_count
|
||||||
|
|
||||||
|
|
||||||
|
@then('"tags" in the xml output should contain {expected_tags_json_list}')
|
||||||
|
def assert_xml_output_tags(context, expected_tags_json_list):
|
||||||
|
output = context.stdout_capture.getvalue()
|
||||||
|
xml_tree = ElementTree.fromstring(output)
|
||||||
|
|
||||||
|
xml_tags = (node.tag for node in xml_tree)
|
||||||
|
assert "tags" in xml_tags, str(list(xml_tags))
|
||||||
|
|
||||||
|
expected_tags = json.loads(expected_tags_json_list)
|
||||||
|
actual_tags = set(t.attrib["name"] for t in xml_tree.find("tags"))
|
||||||
|
assert actual_tags == set(expected_tags), [actual_tags, set(expected_tags)]
|
||||||
|
|
||||||
|
|
||||||
|
@given('we created a directory named "{dir_name}"')
|
||||||
|
def create_directory(context, dir_name):
|
||||||
|
if os.path.exists(dir_name):
|
||||||
|
shutil.rmtree(dir_name)
|
||||||
|
os.mkdir(dir_name)
|
||||||
|
|
||||||
|
|
||||||
|
@then('"{dir_name}" should contain the files {expected_files_json_list}')
|
||||||
|
def assert_dir_contains_files(context, dir_name, expected_files_json_list):
|
||||||
|
actual_files = os.listdir(dir_name)
|
||||||
|
expected_files = json.loads(expected_files_json_list)
|
||||||
|
assert actual_files == expected_files, [actual_files, expected_files]
|
||||||
|
|
||||||
|
|
||||||
|
@then('the content of exported yaml "{file_path}" should be')
|
||||||
|
def assert_exported_yaml_file_content(context, file_path):
|
||||||
|
expected_content = context.text.strip().splitlines()
|
||||||
|
|
||||||
|
with open(file_path, "r") as f:
|
||||||
|
actual_content = f.read().strip().splitlines()
|
||||||
|
|
||||||
|
for actual_line, expected_line in zip(actual_content, expected_content):
|
||||||
|
if actual_line.startswith("tags: ") and expected_line.startswith("tags: "):
|
||||||
|
assert_equal_tags_ignoring_order(actual_line, expected_line)
|
||||||
|
else:
|
||||||
|
assert actual_line.strip() == expected_line.strip(), [
|
||||||
|
actual_line.strip(),
|
||||||
|
expected_line.strip(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def assert_equal_tags_ignoring_order(actual_line, expected_line):
|
||||||
|
actual_tags = set(tag.strip() for tag in actual_line[len("tags: ") :].split(","))
|
||||||
|
expected_tags = set(
|
||||||
|
tag.strip() for tag in expected_line[len("tags: ") :].split(",")
|
||||||
|
)
|
||||||
|
assert actual_tags == expected_tags, [actual_tags, expected_tags]
|
|
@ -1,62 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
from .util import ERROR_COLOR, RESET_COLOR
|
|
||||||
from .util import slugify
|
|
||||||
from .plugins.template import Template
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
class Exporter:
|
|
||||||
"""This Exporter can convert entries and journals into text files."""
|
|
||||||
|
|
||||||
def __init__(self, format):
|
|
||||||
with open("jrnl/templates/" + format + ".template") as f:
|
|
||||||
front_matter, body = f.read().strip("-\n").split("---", 2)
|
|
||||||
self.template = Template(body)
|
|
||||||
|
|
||||||
def export_entry(self, entry):
|
|
||||||
"""Returns a string representation of a single entry."""
|
|
||||||
return str(entry)
|
|
||||||
|
|
||||||
def _get_vars(self, journal):
|
|
||||||
return {"journal": journal, "entries": journal.entries, "tags": journal.tags}
|
|
||||||
|
|
||||||
def export_journal(self, journal):
|
|
||||||
"""Returns a string representation of an entire journal."""
|
|
||||||
return self.template.render_block("journal", **self._get_vars(journal))
|
|
||||||
|
|
||||||
def write_file(self, journal, path):
|
|
||||||
"""Exports a journal into a single file."""
|
|
||||||
try:
|
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(self.export_journal(journal))
|
|
||||||
return f"[Journal exported to {path}]"
|
|
||||||
except OSError as e:
|
|
||||||
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
|
||||||
|
|
||||||
def make_filename(self, entry):
|
|
||||||
return entry.date.strftime(
|
|
||||||
"%Y-%m-%d_{}.{}".format(slugify(entry.title), self.extension)
|
|
||||||
)
|
|
||||||
|
|
||||||
def write_files(self, journal, path):
|
|
||||||
"""Exports a journal into individual files for each entry."""
|
|
||||||
for entry in journal.entries:
|
|
||||||
try:
|
|
||||||
full_path = os.path.join(path, self.make_filename(entry))
|
|
||||||
with open(full_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(self.export_entry(entry))
|
|
||||||
except OSError as e:
|
|
||||||
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
|
||||||
return f"[Journal exported to {path}]"
|
|
||||||
|
|
||||||
def export(self, journal, format="text", output=None):
|
|
||||||
"""Exports to individual files if output is an existing path, or into
|
|
||||||
a single file if output is a file name, or returns the exporter's
|
|
||||||
representation as string if output is None."""
|
|
||||||
if output and os.path.isdir(output): # multiple files
|
|
||||||
return self.write_files(journal, output)
|
|
||||||
elif output: # single file
|
|
||||||
return self.write_file(journal, output)
|
|
||||||
else:
|
|
||||||
return self.export_journal(journal)
|
|
Loading…
Add table
Reference in a new issue