From a959ed49fd84f6c2db7d64650e8d4a4e5873685f Mon Sep 17 00:00:00 2001 From: Aniket Pant Date: Fri, 21 Jun 2013 19:20:57 +0530 Subject: [PATCH 01/91] Add default export folder config Signed-off-by: Aniket Pant --- jrnl/Journal.py | 1 + jrnl/install.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 9cbe41e2..732a491e 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -45,6 +45,7 @@ class Journal(object): 'tagsymbols': '@', 'highlight': True, 'linewrap': 80, + 'folder': os.path.expanduser("~/journal/"), } self.config.update(kwargs) diff --git a/jrnl/install.py b/jrnl/install.py index acb519a9..53b98052 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -32,6 +32,7 @@ default_config = { 'tagsymbols': '@', 'highlight': True, 'linewrap': 80, + 'folder': os.path.expanduser("~/journal/"), } @@ -94,4 +95,7 @@ def install_jrnl(config_path='~/.jrnl_config'): config['password'] = password return config - + # Where to export files? + path_query = 'Path to your journal folder (leave blank for ~/journal): ' + folder_path = raw_input(path_query).strip() or os.path.expanduser('~/journal') + default_config['folder'] = os.path.expanduser(folder_path) From 5408f60db3f5828a7b57a57b582fbf3e33bf3fd5 Mon Sep 17 00:00:00 2001 From: Aniket Pant Date: Fri, 21 Jun 2013 19:22:02 +0530 Subject: [PATCH 02/91] Add new export option Support for json and md retained All export types moved to --export except tags Signed-off-by: Aniket Pant --- jrnl/exporters.py | 44 +++++++++++++++++++++++++++++++++++++++++--- jrnl/jrnl.py | 20 +++++++++++++------- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/jrnl/exporters.py b/jrnl/exporters.py index 4a4b0245..bba105a0 100644 --- a/jrnl/exporters.py +++ b/jrnl/exporters.py @@ -1,6 +1,9 @@ #!/usr/bin/env python # encoding: utf-8 +import os +try: from slugify import slugify +except ImportError: import slugify try: import simplejson as json except ImportError: import json @@ -28,16 +31,18 @@ def to_tag_list(journal): result += "\n".join(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=False)) return result -def to_json(journal): +def to_json(journal, output): """Returns a JSON representation of the Journal.""" tags = get_tags_count(journal) result = { "tags": dict((tag, count) for count, tag in tags), "entries": [e.to_dict() for e in journal.entries] } + if output is not False: + write_file(json.dumps(result, indent=2), output) return json.dumps(result, indent=2) -def to_md(journal): +def to_md(journal, output): """Returns a markdown representation of the Journal""" out = [] year, month = -1, -1 @@ -51,4 +56,37 @@ def to_md(journal): out.append(e.date.strftime("%B")) out.append('-' * len(e.date.strftime("%B")) + "\n") out.append(e.to_md()) - return "\n".join(out) + result = "\n".join(out) + if output is not False: + write_file(result, output) + return result + +def to_txt(journal, output): + """Returns the complete text of the Journal.""" + if output is not False: + write_file(unicode(journal), output) + return unicode(journal) + +def to_files(journal, output): + """Turns your journal into separate files for each entry.""" + if output is False: + output = journal.config['folder'] + "*.txt" # default path + path, extension = os.path.splitext(os.path.expanduser(output)) + head, tail = os.path.split(path) + if tail == '*': # if wildcard is specified + path = head + '/' + if not os.path.exists(path): # if the folder doesn't exist, create it + os.makedirs(path) + for e in journal.entries: + date = e.date.strftime('%Y-%m-%d') + title = slugify(unicode(e.title)) + filename = date + '-' + title + fullpath = path + filename + extension + write_file(unicode(e), fullpath) + return ("Journal exported") + +def write_file(content, path): + """Writes content to the file provided""" + f = open(path, 'w+') + f.write(content) + f.close() diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 2af9b60c..f23cc799 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -45,8 +45,8 @@ def parse_args(): exporting = parser.add_argument_group('Export / Import', 'Options for transmogrifying your journal') exporting.add_argument('--tags', dest='tags', action="store_true", help='Returns a list of all tags and number of occurences') - exporting.add_argument('--json', dest='json', action="store_true", help='Returns a JSON-encoded version of the Journal') - exporting.add_argument('--markdown', dest='markdown', action="store_true", help='Returns a Markdown-formated version of the Journal') + exporting.add_argument('--export', metavar='TYPE', dest='export', help='Export your journal to Markdown, JSON, Text or multiple files', nargs='?', default=False, const=None) + exporting.add_argument('-o', metavar='OUTPUT', dest='output', help='The output of the file can be provided when using with --export', nargs='?', default=False, const=None) exporting.add_argument('--encrypt', metavar='FILENAME', dest='encrypt', help='Encrypts your existing journal with a new password', nargs='?', default=False, const=None) exporting.add_argument('--decrypt', metavar='FILENAME', dest='decrypt', help='Decrypts your journal and stores it in plain text', nargs='?', default=False, const=None) exporting.add_argument('--delete-last', dest='delete_last', help='Deletes the last entry from your journal file.', action="store_true") @@ -57,7 +57,7 @@ def guess_mode(args, config): """Guesses the mode (compose, read or export) from the given arguments""" compose = True export = False - if args.json or args.decrypt is not False or args.encrypt is not False or args.markdown or args.tags or args.delete_last: + if args.decrypt is not False or args.encrypt is not False or args.export is not False or args.tags or args.delete_last: compose = False export = True elif args.start_date or args.end_date or args.limit or args.strict or args.short: @@ -190,11 +190,17 @@ def cli(): elif args.tags: print(exporters.to_tag_list(journal)) - elif args.json: # export to json - print(exporters.to_json(journal)) + elif args.export == 'json': # export to json + print(exporters.to_json(journal, args.output)) - elif args.markdown: # export to json - print(exporters.to_md(journal)) + elif args.export == 'markdown' or args.export == 'md': # export to markdown + print(exporters.to_md(journal, args.output)) + + elif args.export == 'text' or args.export == 'txt': # export to text + print(exporters.to_txt(journal, args.output)) + + elif args.export == 'files': # export to files + print(exporters.to_files(journal, args.output)) elif (args.encrypt is not False or args.decrypt is not False) and not PYCRYPTO: print("PyCrypto not found. To encrypt or decrypt your journal, install the PyCrypto package from http://www.pycrypto.org.") From 65760eabff975956910c865606614ae47ba953a9 Mon Sep 17 00:00:00 2001 From: Aniket Pant Date: Fri, 21 Jun 2013 19:22:28 +0530 Subject: [PATCH 03/91] Add slugify to requirements Signed-off-by: Aniket Pant --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index a4a16eb3..7533fb7a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ parsedatetime >= 1.1.2 colorama >= 0.2.5 pycrypto >= 2.6 argparse==1.2.1 +slugify==0.0.1 From d48f5137e5d524e40436de40f563c2167935a5dd Mon Sep 17 00:00:00 2001 From: Aniket Pant Date: Fri, 21 Jun 2013 19:23:27 +0530 Subject: [PATCH 04/91] Update readme Information on new export options added Signed-off-by: Aniket Pant --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 903c3618..58c7a7d2 100644 --- a/README.md +++ b/README.md @@ -114,16 +114,59 @@ you'll get a list of all tags you used in your journal, sorted by most frequent. Can do: - jrnl --json + jrnl --export json Why not create a beautiful [timeline](http://timeline.verite.co/) of your journal? ### Markdown export - jrnl --markdown +Use: + + jrnl --export markdown + +or + + jrnl --export md Markdown is a simple markup language that is human readable and can be used to be rendered to other formats (html, pdf). This README for example is formatted in markdown and github makes it look nice. +### Text export + + jrnl --export text + +or + + jrnl --export txt + +Prettyprints your entire journal. + +### Export to multiple files + + jrnl --export files + +This export option allows you to get your entire journal into individual files for each entry. +By default, files are exported as `txt` files. You can specify any extension. + +**Output file** + +You can specify an output file with your export options. It works with `json`, `markdown`, `txt` and `files` option. + +For using it with multiple files, you can use it like this: + + jrnl --export files -o ~/journal/*.txt + +If you wish to output to a directory without specifying a file: + + jrnl --export files -o ~/journal/ + +Other methods to use this option + + jrnl --export json -o jrnl.json + jrnl --export md -o jrnl.md + jrnl --export txt -o jrnl.txt + +These will output the given filename in the current working directory. + Encryption ---------- From 99dd9a05aa9127ce86aa662b2896576a3618bc15 Mon Sep 17 00:00:00 2001 From: Aniket Pant Date: Fri, 21 Jun 2013 19:23:46 +0530 Subject: [PATCH 05/91] Update version to 1.1.1 Signed-off-by: Aniket Pant --- jrnl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 4c3a2b79..79103a7b 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.1.0' +__version__ = '1.1.1' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From cf4592fbe303637d76c0bfece740f6850e593158 Mon Sep 17 00:00:00 2001 From: Aniket Pant Date: Fri, 21 Jun 2013 19:23:54 +0530 Subject: [PATCH 06/91] Update changelog Signed-off-by: Aniket Pant --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4de8d5f..5571d2a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ Changelog ========= +#### 1.1.1 + +* [New] Export to multiple files +* [New] Feature to export to given output file + #### 1.1.0 * [New] JSON export exports tags as well. From d7dfba008c60a9d128a52a7b7bdc532e77c5b917 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 24 Jun 2013 12:48:23 +0200 Subject: [PATCH 07/91] Addresses unicode issues in Python 3 Fixes #79 --- .travis.yml | 2 -- CHANGELOG.md | 6 +++++- jrnl/Entry.py | 2 +- jrnl/Journal.py | 6 +++--- jrnl/__init__.py | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index b1ed5780..ecfc9bde 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,10 @@ language: python python: - "2.6" - "2.7" - - "3.2" - "3.3" install: "pip install -r requirements.txt --use-mirrors" # command to run tests script: nosetests matrix: allow_failures: # python 3 support for travis is shaky.... - - python: 3.2 - python: 3.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index c4de8d5f..4faef2fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ Changelog ========= -#### 1.1.0 +#### 1.1.1 + +* [Fixed] Unicode and Python3 issues resolved. + +### 1.1.0 * [New] JSON export exports tags as well. * [Improved] Nicer error message when there is a syntactical error in your config file. diff --git a/jrnl/Entry.py b/jrnl/Entry.py index 624b6197..1d475501 100644 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -15,7 +15,7 @@ class Entry: def parse_tags(self): fulltext = " ".join([self.title, self.body]).lower() - tags = re.findall(ur'([{}]\w+)'.format(self.journal.config['tagsymbols']), fulltext, re.UNICODE) + tags = re.findall(r'(?u)([{}]\w+)'.format(self.journal.config['tagsymbols']), fulltext, re.UNICODE) self.tags = set(tags) def __unicode__(self): diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 9cbe41e2..193d89a6 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -170,13 +170,13 @@ class Journal(object): lambda match: self._colorize(match.group(0)), pp, re.UNICODE) else: - pp = re.sub(ur"(?u)([{}]\w+)".format(self.config['tagsymbols']), + pp = re.sub(r"(?u)([{}]\w+)".format(self.config['tagsymbols']), lambda match: self._colorize(match.group(0)), pp) return pp def __repr__(self): - return "" % len(self.entries) + return "".format(len(self.entries)) def write(self, filename=None): """Dumps the journal into the config file, overwriting it""" @@ -229,7 +229,7 @@ class Journal(object): for m in matches: date = e.date.strftime(self.config['timeformat']) excerpt = e.body[m.start():min(len(e.body), m.end()+60)] - res.append('%s %s ..' % (date, excerpt)) + res.append('{} {} ..'.format(date, excerpt)) e.body = "\n".join(res) else: for e in self.entries: diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 4c3a2b79..79103a7b 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.1.0' +__version__ = '1.1.1' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From 9e969786f5d3dc2ad2b762b31133287c17603e33 Mon Sep 17 00:00:00 2001 From: Aniket Pant Date: Wed, 26 Jun 2013 11:20:58 +0530 Subject: [PATCH 08/91] Fix changelog Signed-off-by: Aniket Pant --- CHANGELOG.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a4ca086..1d83cc4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,16 +3,11 @@ Changelog #### 1.1.1 -<<<<<<< HEAD * [New] Export to multiple files * [New] Feature to export to given output file - -#### 1.1.0 -======= * [Fixed] Unicode and Python3 issues resolved. ### 1.1.0 ->>>>>>> master * [New] JSON export exports tags as well. * [Improved] Nicer error message when there is a syntactical error in your config file. From bdc0b4c5f5ae6e6a014c3c36d202482d42c40d5e Mon Sep 17 00:00:00 2001 From: Aniket Pant Date: Wed, 26 Jun 2013 13:04:50 +0530 Subject: [PATCH 09/91] Remove default folder config Signed-off-by: Aniket Pant --- jrnl/Journal.py | 1 - 1 file changed, 1 deletion(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index ce5f9634..193d89a6 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -45,7 +45,6 @@ class Journal(object): 'tagsymbols': '@', 'highlight': True, 'linewrap': 80, - 'folder': os.path.expanduser("~/journal/"), } self.config.update(kwargs) From 659f9c4d9829dfe887e270ffa3a9b9ef1717fae6 Mon Sep 17 00:00:00 2001 From: Aniket Pant Date: Wed, 26 Jun 2013 13:05:13 +0530 Subject: [PATCH 10/91] Improve export functionality Signed-off-by: Aniket Pant --- jrnl/exporters.py | 90 ++++++++++++++++++++++++++++++++++++----------- jrnl/jrnl.py | 5 +-- 2 files changed, 71 insertions(+), 24 deletions(-) diff --git a/jrnl/exporters.py b/jrnl/exporters.py index bba105a0..85efd43b 100644 --- a/jrnl/exporters.py +++ b/jrnl/exporters.py @@ -2,6 +2,7 @@ # encoding: utf-8 import os +import string try: from slugify import slugify except ImportError: import slugify try: import simplejson as json @@ -20,7 +21,7 @@ def get_tags_count(journal): return tag_counts def to_tag_list(journal): - """Prints a list of all tags and the number of occurances.""" + """Prints a list of all tags and the number of occurrences.""" tag_counts = get_tags_count(journal) result = "" if not tag_counts: @@ -39,8 +40,14 @@ def to_json(journal, output): "entries": [e.to_dict() for e in journal.entries] } if output is not False: - write_file(json.dumps(result, indent=2), output) - return json.dumps(result, indent=2) + path = output_path('json', output) + if not is_globable(path): + message = write_file(json.dumps(result, indent=2), path) + else: + message = to_files(journal, path) + return message + else: + return json.dumps(result, indent=2) def to_md(journal, output): """Returns a markdown representation of the Journal""" @@ -58,35 +65,78 @@ def to_md(journal, output): out.append(e.to_md()) result = "\n".join(out) if output is not False: - write_file(result, output) - return result + path = output_path('md', output) + if not is_globable(path): + message = write_file(result, path) + else: + message = to_files(journal, path) + return message + else: + return result def to_txt(journal, output): """Returns the complete text of the Journal.""" if output is not False: - write_file(unicode(journal), output) - return unicode(journal) + path = output_path('txt', output) + if not is_globable(path): + message = write_file(unicode(journal), path) + else: + message = to_files(journal, path) + return message + else: + return unicode(journal) def to_files(journal, output): """Turns your journal into separate files for each entry.""" - if output is False: - output = journal.config['folder'] + "*.txt" # default path + path, extension = os.path.splitext(os.path.expanduser(output)) + + for e in journal.entries: + content = "" + date = e.date.strftime('%C-%m-%d') + title = slugify(unicode(e.title)) + + filename = string.replace(path, "%C-%m-%d", date) + filename = string.replace(filename, "slug", title) + + fullpath = filename + extension + + if extension == '.json': + content = json.dumps(e.to_dict(), indent=2) + elif extension == '.md': + content = e.to_md() + elif extension == '.txt': + content = unicode(e) + write_file(content, fullpath) + + return ("Journal exported.") + +def is_globable(output): path, extension = os.path.splitext(os.path.expanduser(output)) head, tail = os.path.split(path) - if tail == '*': # if wildcard is specified - path = head + '/' - if not os.path.exists(path): # if the folder doesn't exist, create it - os.makedirs(path) - for e in journal.entries: - date = e.date.strftime('%Y-%m-%d') - title = slugify(unicode(e.title)) - filename = date + '-' + title - fullpath = path + filename + extension - write_file(unicode(e), fullpath) - return ("Journal exported") + + if tail == "%C-%m-%d_slug": + return True + else: + return False + +def output_path(file_ext, output): + path, extension = os.path.splitext(os.path.expanduser(output)) + + head, tail = os.path.split(path) + if head != '': + if not os.path.exists(head): # if the folder doesn't exist, create it + os.makedirs(head) + fullpath = head + '/' + tail + '.' + file_ext + else: + fullpath = tail + '.' + file_ext + + return fullpath def write_file(content, path): """Writes content to the file provided""" + f = open(path, 'w+') f.write(content) f.close() + + return ("File exported to " + path) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index f23cc799..eafc05e4 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -45,7 +45,7 @@ def parse_args(): exporting = parser.add_argument_group('Export / Import', 'Options for transmogrifying your journal') exporting.add_argument('--tags', dest='tags', action="store_true", help='Returns a list of all tags and number of occurences') - exporting.add_argument('--export', metavar='TYPE', dest='export', help='Export your journal to Markdown, JSON, Text or multiple files', nargs='?', default=False, const=None) + exporting.add_argument('--export', metavar='TYPE', dest='export', help='Export your journal to Markdown, JSON or Text', nargs='?', default=False, const=None) exporting.add_argument('-o', metavar='OUTPUT', dest='output', help='The output of the file can be provided when using with --export', nargs='?', default=False, const=None) exporting.add_argument('--encrypt', metavar='FILENAME', dest='encrypt', help='Encrypts your existing journal with a new password', nargs='?', default=False, const=None) exporting.add_argument('--decrypt', metavar='FILENAME', dest='decrypt', help='Decrypts your journal and stores it in plain text', nargs='?', default=False, const=None) @@ -199,9 +199,6 @@ def cli(): elif args.export == 'text' or args.export == 'txt': # export to text print(exporters.to_txt(journal, args.output)) - elif args.export == 'files': # export to files - print(exporters.to_files(journal, args.output)) - elif (args.encrypt is not False or args.decrypt is not False) and not PYCRYPTO: print("PyCrypto not found. To encrypt or decrypt your journal, install the PyCrypto package from http://www.pycrypto.org.") From 6fff395701ee595aaf4bfca70c0f57cd55894b18 Mon Sep 17 00:00:00 2001 From: Aniket Pant Date: Wed, 26 Jun 2013 13:05:24 +0530 Subject: [PATCH 11/91] Update readme in accordance to changes Signed-off-by: Aniket Pant --- README.md | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 58c7a7d2..3d6492cf 100644 --- a/README.md +++ b/README.md @@ -140,32 +140,17 @@ or Prettyprints your entire journal. -### Export to multiple files +### Export to files - jrnl --export files + jrnl --export md -o journal -This export option allows you to get your entire journal into individual files for each entry. -By default, files are exported as `txt` files. You can specify any extension. +The above command will generate a file named `journal.md`. The extension will be generated on the type of export option. This way a json export will generate a `.json` file and a plain text export will generate a `.txt` file. -**Output file** +In case you wish to export to multiple files, you can use a glob-able filename. -You can specify an output file with your export options. It works with `json`, `markdown`, `txt` and `files` option. + jrnl --export markdown -o %C-%m-%d_slug -For using it with multiple files, you can use it like this: - - jrnl --export files -o ~/journal/*.txt - -If you wish to output to a directory without specifying a file: - - jrnl --export files -o ~/journal/ - -Other methods to use this option - - jrnl --export json -o jrnl.json - jrnl --export md -o jrnl.md - jrnl --export txt -o jrnl.txt - -These will output the given filename in the current working directory. +It also works with `json` and `text` export types. Encryption ---------- From bce370174b18d272bf04a4ecda37921d97f0f0dc Mon Sep 17 00:00:00 2001 From: Aniket Pant Date: Wed, 26 Jun 2013 13:08:29 +0530 Subject: [PATCH 12/91] Update installation after removal of default folder config Signed-off-by: Aniket Pant --- jrnl/install.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/jrnl/install.py b/jrnl/install.py index 53b98052..2037e8aa 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -32,7 +32,6 @@ default_config = { 'tagsymbols': '@', 'highlight': True, 'linewrap': 80, - 'folder': os.path.expanduser("~/journal/"), } @@ -94,8 +93,3 @@ def install_jrnl(config_path='~/.jrnl_config'): if password: config['password'] = password return config - - # Where to export files? - path_query = 'Path to your journal folder (leave blank for ~/journal): ' - folder_path = raw_input(path_query).strip() or os.path.expanduser('~/journal') - default_config['folder'] = os.path.expanduser(folder_path) From 42daea2dd6caf4c15e9d52cfe6a7220e2865e25c Mon Sep 17 00:00:00 2001 From: dejay Date: Sat, 13 Jul 2013 11:57:34 -0600 Subject: [PATCH 13/91] Better Day One Timestamps --- jrnl/Journal.py | 12 ++++++++++-- requirements.txt | 3 +++ setup.py | 2 ++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 193d89a6..6141b5d6 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -10,6 +10,8 @@ except ImportError: import parsedatetime.parsedatetime as pdt import re from datetime import datetime import time +import pytz +from tzlocal import get_localzone try: import simplejson as json except ImportError: import json import sys @@ -308,7 +310,10 @@ class DayOne(Journal): for filename in filenames: with open(filename) as plist_entry: dict_entry = plistlib.readPlist(plist_entry) - entry = self.new_entry(raw=dict_entry['Entry Text'], date=dict_entry['Creation Date'], sort=False) + timezone = pytz.timezone(dict_entry['Time Zone']) + date = dict_entry['Creation Date'] + date = date + timezone.utcoffset(date) + entry = self.new_entry(raw=dict_entry['Entry Text'], date=date, sort=False) entry.starred = dict_entry["Starred"] entry.uuid = dict_entry["UUID"] # We're using new_entry to create the Entry object, which adds the entry @@ -324,12 +329,15 @@ class DayOne(Journal): # that have a uuid will be old ones, and only the one that doesn't will # have a new one! if not hasattr(entry, "uuid"): + timezone = str(get_localzone()) + utc_time = datetime.utcfromtimestamp(time.mktime(entry.date.timetuple())) new_uuid = uuid.uuid1().hex filename = os.path.join(self.config['journal'], "entries", new_uuid+".doentry") entry_plist = { - 'Creation Date': entry.date, + 'Creation Date': utc_time, 'Starred': entry.starred if hasattr(entry, 'starred') else False, 'Entry Text': entry.title+"\n"+entry.body, + 'Time Zone': timezone, 'UUID': new_uuid } plistlib.writePlist(entry_plist, filename) diff --git a/requirements.txt b/requirements.txt index a4a16eb3..0871fab2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ parsedatetime >= 1.1.2 +pytz >= 2013b colorama >= 0.2.5 pycrypto >= 2.6 argparse==1.2.1 +tzlocal == 1.0 + diff --git a/setup.py b/setup.py index 2b0bbd11..0bb14c25 100644 --- a/setup.py +++ b/setup.py @@ -70,6 +70,8 @@ setup( packages = ['jrnl'], install_requires = [ "parsedatetime>=1.1.2", + "pytz>=2013b", + "tzlocal==1.0", "colorama>=0.2.5" ] + [p for p, cond in conditional_dependencies.items() if cond], extras_require = { From f6be2f13a5145e02e5bc2247ff06f53ea322eb7a Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 15 Jul 2013 13:32:23 +0200 Subject: [PATCH 14/91] Workaround for get_localzone on OS X --- jrnl/Journal.py | 7 +++---- jrnl/util.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 6141b5d6..ce10f3d1 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -3,6 +3,7 @@ try: from . import Entry except (SystemError, ValueError): import Entry +from util import get_local_timezone import codecs import os try: import parsedatetime.parsedatetime_consts as pdt @@ -10,8 +11,6 @@ except ImportError: import parsedatetime.parsedatetime as pdt import re from datetime import datetime import time -import pytz -from tzlocal import get_localzone try: import simplejson as json except ImportError: import json import sys @@ -32,6 +31,7 @@ try: except ImportError: colorama = None import plistlib +import pytz import uuid @@ -329,7 +329,6 @@ class DayOne(Journal): # that have a uuid will be old ones, and only the one that doesn't will # have a new one! if not hasattr(entry, "uuid"): - timezone = str(get_localzone()) utc_time = datetime.utcfromtimestamp(time.mktime(entry.date.timetuple())) new_uuid = uuid.uuid1().hex filename = os.path.join(self.config['journal'], "entries", new_uuid+".doentry") @@ -337,7 +336,7 @@ class DayOne(Journal): 'Creation Date': utc_time, 'Starred': entry.starred if hasattr(entry, 'starred') else False, 'Entry Text': entry.title+"\n"+entry.body, - 'Time Zone': timezone, + 'Time Zone': get_local_timezone(), 'UUID': new_uuid } plistlib.writePlist(entry_plist, filename) diff --git a/jrnl/util.py b/jrnl/util.py index 391cbc17..973e7fd1 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # encoding: utf-8 import sys +import os +from tzlocal import get_localzone def py23_input(msg): if sys.version_info[0] == 3: @@ -8,3 +10,12 @@ def py23_input(msg): except SyntaxError: return "" else: return raw_input(msg) + +def get_local_timezone(): + """Returns the Olson identifier of the local timezone. + In a happy world, tzlocal.get_localzone would do this, but there's a bug on OS X + that prevents that right now: https://github.com/regebro/tzlocal/issues/6""" + if "darwin" in sys.platform: + return os.popen("systemsetup -gettimezone").read().replace("Time Zone: ", "").strip() + else: + return str(get_localzone()) From d81f263e8c4867bbd86f554388ba6f8a3e9d21f3 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 15 Jul 2013 13:37:13 +0200 Subject: [PATCH 15/91] Uses local timezone if timezone could not be parsed --- jrnl/Journal.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index ce10f3d1..88d1b6c7 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -310,7 +310,10 @@ class DayOne(Journal): for filename in filenames: with open(filename) as plist_entry: dict_entry = plistlib.readPlist(plist_entry) - timezone = pytz.timezone(dict_entry['Time Zone']) + try: + timezone = pytz.timezone(dict_entry['Time Zone']) + except pytz.exceptions.UnknownTimeZoneError: + timezone = pytz.timezone(get_local_timezone()) date = dict_entry['Creation Date'] date = date + timezone.utcoffset(date) entry = self.new_entry(raw=dict_entry['Entry Text'], date=date, sort=False) From de8caf08a6aca2dccadf11ef25641c9e705fdf05 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 15 Jul 2013 13:37:30 +0200 Subject: [PATCH 16/91] Caches local timezone --- jrnl/util.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/jrnl/util.py b/jrnl/util.py index 973e7fd1..9ed4e1f6 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -4,6 +4,8 @@ import sys import os from tzlocal import get_localzone +__cached_tz = None + def py23_input(msg): if sys.version_info[0] == 3: try: return input(msg) @@ -15,7 +17,9 @@ def get_local_timezone(): """Returns the Olson identifier of the local timezone. In a happy world, tzlocal.get_localzone would do this, but there's a bug on OS X that prevents that right now: https://github.com/regebro/tzlocal/issues/6""" - if "darwin" in sys.platform: - return os.popen("systemsetup -gettimezone").read().replace("Time Zone: ", "").strip() - else: - return str(get_localzone()) + global __cached_tz + if not __cached_tz and "darwin" in sys.platform: + __cached_tz = os.popen("systemsetup -gettimezone").read().replace("Time Zone: ", "").strip() + elif not __cached_tz: + __cached_tz = str(get_localzone()) + return __cached_tz From a7e64dd75607cc556a8730fecb87fece6a9e7d99 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 15 Jul 2013 13:37:35 +0200 Subject: [PATCH 17/91] Version bump --- CHANGELOG.md | 4 ++++ jrnl/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4faef2fa..903bdb24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +#### 1.1.2 + +* [Fixed] Timezone support for DayOne + #### 1.1.1 * [Fixed] Unicode and Python3 issues resolved. diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 79103a7b..09f0d8fe 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.1.1' +__version__ = '1.2.0' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From 39a8b3a4cde15c4fc76c32d883e0512d8be3dad3 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 15 Jul 2013 13:55:34 +0200 Subject: [PATCH 18/91] Oh Python 3 imports... --- jrnl/Journal.py | 5 +++-- jrnl/__init__.py | 2 +- tmp_log.bak | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 tmp_log.bak diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 88d1b6c7..d4e733ea 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -3,7 +3,8 @@ try: from . import Entry except (SystemError, ValueError): import Entry -from util import get_local_timezone +try: from .util import get_local_timezone +except (SystemError, ValueError): from util import get_local_timezone import codecs import os try: import parsedatetime.parsedatetime_consts as pdt @@ -308,7 +309,7 @@ class DayOne(Journal): of filenames, interpret each as a plist file and create a new entry from that.""" self.entries = [] for filename in filenames: - with open(filename) as plist_entry: + with open(filename, 'rb') as plist_entry: dict_entry = plistlib.readPlist(plist_entry) try: timezone = pytz.timezone(dict_entry['Time Zone']) diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 09f0d8fe..7f9e2402 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.2.0' +__version__ = '1.2.1' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' diff --git a/tmp_log.bak b/tmp_log.bak new file mode 100644 index 00000000..3d3a44c9 --- /dev/null +++ b/tmp_log.bak @@ -0,0 +1 @@ + From d56f4140fb3c0cd389ad1e64ed452dadb968c2c3 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 17 Jul 2013 18:13:51 +0200 Subject: [PATCH 19/91] Fixes bug in tag export --- jrnl/Entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jrnl/Entry.py b/jrnl/Entry.py index 1d475501..a8ec0557 100644 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -16,7 +16,7 @@ class Entry: def parse_tags(self): fulltext = " ".join([self.title, self.body]).lower() tags = re.findall(r'(?u)([{}]\w+)'.format(self.journal.config['tagsymbols']), fulltext, re.UNICODE) - self.tags = set(tags) + return set(tags) def __unicode__(self): """Returns a string representation of the entry to be written into a journal file.""" @@ -57,7 +57,7 @@ class Entry: ) def __repr__(self): - return str(self) + return "".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M")) def to_dict(self): return { From 0c374a839cb8cd7f683e5e4fd78ff57f26151c58 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 17 Jul 2013 18:19:14 +0200 Subject: [PATCH 20/91] Simplified exporting logic --- jrnl/exporters.py | 125 +++++++++++++++++----------------------------- jrnl/jrnl.py | 10 +--- 2 files changed, 47 insertions(+), 88 deletions(-) diff --git a/jrnl/exporters.py b/jrnl/exporters.py index 85efd43b..5bd687cf 100644 --- a/jrnl/exporters.py +++ b/jrnl/exporters.py @@ -32,24 +32,16 @@ def to_tag_list(journal): result += "\n".join(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=False)) return result -def to_json(journal, output): +def to_json(journal): """Returns a JSON representation of the Journal.""" tags = get_tags_count(journal) result = { "tags": dict((tag, count) for count, tag in tags), "entries": [e.to_dict() for e in journal.entries] } - if output is not False: - path = output_path('json', output) - if not is_globable(path): - message = write_file(json.dumps(result, indent=2), path) - else: - message = to_files(journal, path) - return message - else: - return json.dumps(result, indent=2) + return json.dumps(result, indent=2) -def to_md(journal, output): +def to_md(journal): """Returns a markdown representation of the Journal""" out = [] year, month = -1, -1 @@ -64,79 +56,52 @@ def to_md(journal, output): out.append('-' * len(e.date.strftime("%B")) + "\n") out.append(e.to_md()) result = "\n".join(out) - if output is not False: - path = output_path('md', output) - if not is_globable(path): - message = write_file(result, path) - else: - message = to_files(journal, path) - return message - else: - return result + return result -def to_txt(journal, output): +def to_txt(journal): """Returns the complete text of the Journal.""" - if output is not False: - path = output_path('txt', output) - if not is_globable(path): - message = write_file(unicode(journal), path) + return unicode(journal) + +def export(journal, format, output=None): + """Exports the journal to various formats. + format should be one of json, txt, text, md, markdown. + If output is None, returns a unicode representation of the output. + If output is a directory, exports entries into individual files. + Otherwise, exports to the given output file. + """ + maps = { + "json": to_json, + "txt": to_txt, + "text": to_txt, + "md": to_md, + "markdown": to_md + } + if output and os.path.isdir(output): # multiple files + return write_files(journal, output, format) + else: + content = maps[format](journal) + if output: + try: + with open(output, 'w') as f: + f.write(content) + return "[Journal exported to {}]".format(output) + except IOError as e: + return "[ERROR: {} {}]".format(e.filename, e.strerror) else: - message = to_files(journal, path) - return message - else: - return unicode(journal) - -def to_files(journal, output): - """Turns your journal into separate files for each entry.""" - path, extension = os.path.splitext(os.path.expanduser(output)) + return content +def write_files(journal, path, format): + """Turns your journal into separate files for each entry. + Format should be either json, md or txt.""" + make_filename = lambda entry: e.date.strftime("%C-%m-%d_{}.{}".format(slugify(unicode(e.title)), format)) for e in journal.entries: - content = "" - date = e.date.strftime('%C-%m-%d') - title = slugify(unicode(e.title)) - - filename = string.replace(path, "%C-%m-%d", date) - filename = string.replace(filename, "slug", title) - - fullpath = filename + extension - - if extension == '.json': - content = json.dumps(e.to_dict(), indent=2) - elif extension == '.md': + full_path = os.path.join(path, make_filename(e)) + if format == 'json': + content = json.dumps(e.to_dict(), indent=2) + "\n" + elif format == 'md': content = e.to_md() - elif extension == '.txt': + elif format == 'txt': content = unicode(e) - write_file(content, fullpath) - - return ("Journal exported.") - -def is_globable(output): - path, extension = os.path.splitext(os.path.expanduser(output)) - head, tail = os.path.split(path) - - if tail == "%C-%m-%d_slug": - return True - else: - return False - -def output_path(file_ext, output): - path, extension = os.path.splitext(os.path.expanduser(output)) - - head, tail = os.path.split(path) - if head != '': - if not os.path.exists(head): # if the folder doesn't exist, create it - os.makedirs(head) - fullpath = head + '/' + tail + '.' + file_ext - else: - fullpath = tail + '.' + file_ext - - return fullpath - -def write_file(content, path): - """Writes content to the file provided""" - - f = open(path, 'w+') - f.write(content) - f.close() - - return ("File exported to " + path) + with open(full_path, 'w') as f: + f.write(content) + return "[Journal exported individual files in {}]".format(path) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index eafc05e4..3377b686 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -190,14 +190,8 @@ def cli(): elif args.tags: print(exporters.to_tag_list(journal)) - elif args.export == 'json': # export to json - print(exporters.to_json(journal, args.output)) - - elif args.export == 'markdown' or args.export == 'md': # export to markdown - print(exporters.to_md(journal, args.output)) - - elif args.export == 'text' or args.export == 'txt': # export to text - print(exporters.to_txt(journal, args.output)) + elif args.export is not False: + print(exporters.export(journal, args.export, args.output)) elif (args.encrypt is not False or args.decrypt is not False) and not PYCRYPTO: print("PyCrypto not found. To encrypt or decrypt your journal, install the PyCrypto package from http://www.pycrypto.org.") From edc41dab20151c07bd6cb5915313d0d5ee1b97a2 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 17 Jul 2013 18:24:53 +0200 Subject: [PATCH 21/91] Updated Readme --- CHANGELOG.md | 7 +++++-- README.md | 15 ++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 321712ff..a44755dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,17 @@ Changelog ========= +### 1.3.0 + +* [New] Export to multiple files +* [New] Feature to export to given output file + #### 1.1.2 * [Fixed] Timezone support for DayOne #### 1.1.1 -* [New] Export to multiple files -* [New] Feature to export to given output file * [Fixed] Unicode and Python3 issues resolved. ### 1.1.0 diff --git a/README.md b/README.md index 3d6492cf..58a03d6d 100644 --- a/README.md +++ b/README.md @@ -142,15 +142,20 @@ Prettyprints your entire journal. ### Export to files - jrnl --export md -o journal +You can specify the output file of your exported journal using the `-o` argument: -The above command will generate a file named `journal.md`. The extension will be generated on the type of export option. This way a json export will generate a `.json` file and a plain text export will generate a `.txt` file. + jrnl --export md -o journal.md -In case you wish to export to multiple files, you can use a glob-able filename. +The above command will generate a file named `journal.md`. If the `-o` argument is a directory, jrnl will export each entry into an individual file: - jrnl --export markdown -o %C-%m-%d_slug + jrnl --export json -o my_entries/ -It also works with `json` and `text` export types. +The contents of `my_entries/` will then look like this: + + my_entries/ + |- 2013_06_03_a-beautiful-day.json + |- 2013_06_07_dinner-with-gabriel.json + |- ... Encryption ---------- From b773c0d7f5ae3b223b8a02ff29d8fb6313b8445f Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 17 Jul 2013 18:29:50 +0200 Subject: [PATCH 22/91] Version bump --- jrnl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 7f9e2402..14ca1bfa 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.2.1' +__version__ = '1.3.0' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From 02ce7dfbd3a03197bdb73ff1353ff0d1de441f27 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 18 Jul 2013 09:02:37 +0200 Subject: [PATCH 23/91] Adds slugify to setup.py --- jrnl/__init__.py | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 14ca1bfa..7ca6b98e 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.3.0' +__version__ = '1.3.1' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' diff --git a/setup.py b/setup.py index 0bb14c25..c6a53760 100644 --- a/setup.py +++ b/setup.py @@ -72,6 +72,7 @@ setup( "parsedatetime>=1.1.2", "pytz>=2013b", "tzlocal==1.0", + "slugify>=0.0.1", "colorama>=0.2.5" ] + [p for p, cond in conditional_dependencies.items() if cond], extras_require = { From c5cd42028d8dae0a414e03ea52638ce5e866d9fd Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 18 Jul 2013 22:48:46 +0200 Subject: [PATCH 24/91] Ability to parse in args manually --- jrnl/jrnl.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 3377b686..e1fa134f 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -29,7 +29,7 @@ xdg_config = os.environ.get('XDG_CONFIG_HOME') CONFIG_PATH = os.path.join(xdg_config, "jrnl") if xdg_config else os.path.expanduser('~/.jrnl_config') PYCRYPTO = install.module_exists("Crypto") -def parse_args(): +def parse_args(args=None): parser = argparse.ArgumentParser() composing = parser.add_argument_group('Composing', 'Will make an entry out of whatever follows as arguments') composing.add_argument('-date', dest='date', help='Date, e.g. "yesterday at 5pm"') @@ -51,7 +51,7 @@ def parse_args(): exporting.add_argument('--decrypt', metavar='FILENAME', dest='decrypt', help='Decrypts your journal and stores it in plain text', nargs='?', default=False, const=None) exporting.add_argument('--delete-last', dest='delete_last', help='Deletes the last entry from your journal file.', action="store_true") - return parser.parse_args() + return parser.parse_args(args) def guess_mode(args, config): """Guesses the mode (compose, read or export) from the given arguments""" @@ -114,7 +114,7 @@ def update_config(config, new_config, scope): config.update(new_config) -def cli(): +def cli(manual_args=None): if not os.path.exists(CONFIG_PATH): config = install.install_jrnl(CONFIG_PATH) else: @@ -133,7 +133,7 @@ def cli(): print("According to your jrnl_conf, your journal is encrypted, however PyCrypto was not found. To open your journal, install the PyCrypto package from http://www.pycrypto.org.") sys.exit(-1) - args = parse_args() + args = parse_args(manual_args) # If the first textual argument points to a journal file, # use this! From 783603fb1c1d8a1717b3343b8e775b4047d10e70 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 18 Jul 2013 22:49:22 +0200 Subject: [PATCH 25/91] Core testing --- features/configs/basic.json | 14 ++++++++++++++ features/core.feature | 14 ++++++++++++++ features/journals/simple.journal | 5 +++++ features/steps/core.py | 24 ++++++++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 features/configs/basic.json create mode 100644 features/core.feature create mode 100644 features/journals/simple.journal create mode 100644 features/steps/core.py diff --git a/features/configs/basic.json b/features/configs/basic.json new file mode 100644 index 00000000..2dc11d73 --- /dev/null +++ b/features/configs/basic.json @@ -0,0 +1,14 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": false, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "", + "journals": { + "default": "features/journals/simple.journal" + }, + "tagsymbols": "@" +} diff --git a/features/core.feature b/features/core.feature new file mode 100644 index 00000000..45aa5e26 --- /dev/null +++ b/features/core.feature @@ -0,0 +1,14 @@ +Feature: Basic reading and writing to a journal + + Scenario: Loading a sample journal + Given we use "basic.json" + When we run "jrnl -n 2" + Then we should get no error + Then the output should be + """ + 2013-06-09 15:39 My first entry. + | Everything is alright + + 2013-06-10 15:40 Life is good. + | But I'm better. + """ diff --git a/features/journals/simple.journal b/features/journals/simple.journal new file mode 100644 index 00000000..66d8439c --- /dev/null +++ b/features/journals/simple.journal @@ -0,0 +1,5 @@ +2013-06-09 15:39 My first entry. +Everything is alright + +2013-06-10 15:40 Life is good. +But I'm better. diff --git a/features/steps/core.py b/features/steps/core.py new file mode 100644 index 00000000..44893d41 --- /dev/null +++ b/features/steps/core.py @@ -0,0 +1,24 @@ +from behave import * +from jrnl import Journal, jrnl +import os + +@given('we use "{config_file}"') +def set_config(context, config_file): + full_path = os.path.join("features/configs", config_file) + jrnl.CONFIG_PATH = os.path.abspath(full_path) + +@when('we run "{command}"') +def run(context, command): + args = command.split()[1:] + jrnl.cli(args) + +@then('we should get no error') +def no_error(context): + assert context.failed is False + +@then('the output should be') +def check_output(context): + text = context.text.strip().splitlines() + out = context.stdout_capture.getvalue().strip().splitlines() + for line_text, line_out in zip(text, out): + assert line_text.strip() == line_out.strip() From 89423bdccbf5fbfc8a645e0066e1082de64a8eac Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 11:36:29 +0200 Subject: [PATCH 26/91] Backup and restore config and journal files every time --- features/environment.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 features/environment.py diff --git a/features/environment.py b/features/environment.py new file mode 100644 index 00000000..f374c25a --- /dev/null +++ b/features/environment.py @@ -0,0 +1,22 @@ +from behave import * +import shutil +import os + +def before_scenario(context, scenario): + """Before each scenario, backup all config and journal test data.""" + for folder in ("configs", "journals"): + original = os.path.join("features", folder) + backup = os.path.join("features", folder+"_backup") + if not os.path.exists(backup): + os.mkdir(backup) + for filename in os.listdir(original): + shutil.copy2(os.path.join(original, filename), backup) + +def after_scenario(context, scenario): + """After each scenario, restore all test data and remove backups.""" + for folder in ("configs", "journals"): + original = os.path.join("features", folder) + backup = os.path.join("features", folder+"_backup") + for filename in os.listdir(backup): + shutil.copy2(os.path.join(backup, filename), original) + shutil.rmtree(backup) From 6521d55a2b743083803ebd6cce8c1b4a3f37ab78 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 11:36:39 +0200 Subject: [PATCH 27/91] Tests for writing entries from the command line --- features/core.feature | 11 +++++++++-- features/steps/core.py | 8 +++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/features/core.feature b/features/core.feature index 45aa5e26..466a1851 100644 --- a/features/core.feature +++ b/features/core.feature @@ -1,10 +1,10 @@ Feature: Basic reading and writing to a journal Scenario: Loading a sample journal - Given we use "basic.json" + Given we use the config "basic.json" When we run "jrnl -n 2" Then we should get no error - Then the output should be + And the output should be """ 2013-06-09 15:39 My first entry. | Everything is alright @@ -12,3 +12,10 @@ Feature: Basic reading and writing to a journal 2013-06-10 15:40 Life is good. | But I'm better. """ + + Scenario: Writing an entry from command line + Given we use the config "basic.json" + When we run "jrnl 23 july 2013: A cold and stormy day. I ate crisps on the sofa." + Then the output should contain "Entry added" + When we run "jrnl -n 1" + Then the output should contain "2013-07-23 09:00 A cold and stormy day." diff --git a/features/steps/core.py b/features/steps/core.py index 44893d41..a4fa5c55 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -2,7 +2,7 @@ from behave import * from jrnl import Journal, jrnl import os -@given('we use "{config_file}"') +@given('we use the config "{config_file}"') def set_config(context, config_file): full_path = os.path.join("features/configs", config_file) jrnl.CONFIG_PATH = os.path.abspath(full_path) @@ -22,3 +22,9 @@ def check_output(context): out = context.stdout_capture.getvalue().strip().splitlines() for line_text, line_out in zip(text, out): assert line_text.strip() == line_out.strip() + +@then('the output should contain "{text}"') +def check_output(context, text): + out = context.stdout_capture.getvalue() + print out + assert text in out From 3a4c87ca4c1f57bcd853ad227f303b09e9dce70f Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 11:46:02 +0200 Subject: [PATCH 28/91] Emoji support --- features/core.feature | 8 ++++++++ jrnl/jrnl.py | 5 +++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/features/core.feature b/features/core.feature index 466a1851..b56c2f4a 100644 --- a/features/core.feature +++ b/features/core.feature @@ -19,3 +19,11 @@ Feature: Basic reading and writing to a journal Then the output should contain "Entry added" When we run "jrnl -n 1" Then the output should contain "2013-07-23 09:00 A cold and stormy day." + + Scenario: Emoji support + Given we use the config "basic.json" + When we run "jrnl 23 july 2013: 🌞 sunny day. Saw an 🐘" + Then the output should contain "Entry added" + When we run "jrnl -n 1" + Then the output should contain "🌞" + and the output should contain "🐘" diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index e1fa134f..45df7a9c 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -171,8 +171,9 @@ def cli(manual_args=None): # Writing mode if mode_compose: raw = " ".join(args.text).strip() - unicode_raw = raw.decode(sys.getfilesystemencoding()) - entry = journal.new_entry(unicode_raw, args.date) + if type(raw) is not unicode: + raw = raw.decode(sys.getfilesystemencoding()) + entry = journal.new_entry(raw, args.date) entry.starred = args.star print("[Entry added to {0} journal]".format(journal_name)) journal.write() From d4cbd4f9442fcbf9b94ef4b34eeb7ab9622516ea Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 12:43:58 +0200 Subject: [PATCH 29/91] Update travis --- .travis.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ecfc9bde..15cf9c92 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,13 @@ python: - "2.6" - "2.7" - "3.3" -install: "pip install -r requirements.txt --use-mirrors" +install: + - "pip install -q -r requirements.txt --use-mirrors" + - "pip install -q behave" # command to run tests -script: nosetests +script: + - python --version + - behave matrix: allow_failures: # python 3 support for travis is shaky.... - python: 3.3 From 6abd7d5b68163c9070067f90eaa9d96f5bfe8d55 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 12:44:39 +0200 Subject: [PATCH 30/91] Mock stdin --- features/core.feature | 9 ++++++++- features/steps/core.py | 33 ++++++++++++++++++++++++++++++--- jrnl/jrnl.py | 2 -- jrnl/util.py | 10 +++++----- tests/test_jrnl.py | 33 --------------------------------- 5 files changed, 43 insertions(+), 44 deletions(-) delete mode 100644 tests/test_jrnl.py diff --git a/features/core.feature b/features/core.feature index b56c2f4a..7e495ef9 100644 --- a/features/core.feature +++ b/features/core.feature @@ -4,7 +4,7 @@ Feature: Basic reading and writing to a journal Given we use the config "basic.json" When we run "jrnl -n 2" Then we should get no error - And the output should be + and the output should be """ 2013-06-09 15:39 My first entry. | Everything is alright @@ -27,3 +27,10 @@ Feature: Basic reading and writing to a journal When we run "jrnl -n 1" Then the output should contain "🌞" and the output should contain "🐘" + + Scenario: Writing an entry at the prompt + Given we use the config "basic.json" + When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive." + Then we should get no error + and the journal should contain "2013-07-25 09:00 I saw Elvis." + and the journal should contain "He's alive." diff --git a/features/steps/core.py b/features/steps/core.py index a4fa5c55..5687e3c9 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -1,16 +1,36 @@ from behave import * -from jrnl import Journal, jrnl +from jrnl import jrnl import os +import sys +import json +import StringIO + +def read_journal(journal_name="default"): + with open(jrnl.CONFIG_PATH) as config_file: + config = json.load(config_file) + with open(config['journals'][journal_name]) as journal_file: + journal = journal_file.read() + return journal @given('we use the config "{config_file}"') def set_config(context, config_file): full_path = os.path.join("features/configs", config_file) jrnl.CONFIG_PATH = os.path.abspath(full_path) +@when('we run "{command}" and enter') +@when('we run "{command}" and enter "{inputs}"') +def run_with_input(context, command, inputs=None): + text = inputs or context.text + args = command.split()[1:] + buffer = StringIO.StringIO(text.strip()) + jrnl.util.STDIN = buffer + jrnl.cli(args) + @when('we run "{command}"') def run(context, command): args = command.split()[1:] - jrnl.cli(args) + jrnl.cli(args or None) + @then('we should get no error') def no_error(context): @@ -24,7 +44,14 @@ def check_output(context): assert line_text.strip() == line_out.strip() @then('the output should contain "{text}"') -def check_output(context, text): +def check_output_inline(context, text): out = context.stdout_capture.getvalue() print out assert text in out + +@then('the journal should contain "{text}"') +@then('journal {journal_name} should contain "{text}"') +def check_journal_content(context, text, journal_name="default"): + journal = read_journal(journal_name) + assert text in journal + diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 45df7a9c..3eba8700 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -149,8 +149,6 @@ def cli(manual_args=None): mode_compose, mode_export = guess_mode(args, config) # open journal file or folder - - if os.path.isdir(config['journal']) and ( config['journal'].endswith(".dayone") or \ config['journal'].endswith(".dayone/")): journal = Journal.DayOne(**config) diff --git a/jrnl/util.py b/jrnl/util.py index 9ed4e1f6..5f7f0c52 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -4,14 +4,14 @@ import sys import os from tzlocal import get_localzone +STDIN = sys.stdin +STDOUT = sys.stdout + __cached_tz = None def py23_input(msg): - if sys.version_info[0] == 3: - try: return input(msg) - except SyntaxError: return "" - else: - return raw_input(msg) + STDOUT.write(msg) + return STDIN.readline().strip() def get_local_timezone(): """Returns the Olson identifier of the local timezone. diff --git a/tests/test_jrnl.py b/tests/test_jrnl.py deleted file mode 100644 index 8280fe6e..00000000 --- a/tests/test_jrnl.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - -import unittest - -class TestClasses(unittest.TestCase): - """Test the behavior of the classes. - - tests related to the Journal and the Entry Classes which can - be tested withouth command-line interaction - """ - - def setUp(self): - pass - - def test_colon_in_textbody(self): - """colons should not cause problems in the text body""" - pass - - -class TestCLI(unittest.TestCase): - """test the command-line interaction part of the program""" - - def setUp(self): - pass - - def test_something(self): - """first test""" - pass - - -if __name__ == '__main__': - unittest.main() From 608cc048976bd9f163dcc6e1cbfefbb40f5fe2d4 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 13:03:27 +0200 Subject: [PATCH 31/91] Better Python2.6 compatibility --- features/steps/core.py | 1 - jrnl/Entry.py | 2 +- jrnl/Journal.py | 5 +++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/features/steps/core.py b/features/steps/core.py index 5687e3c9..4b57b47e 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -46,7 +46,6 @@ def check_output(context): @then('the output should contain "{text}"') def check_output_inline(context, text): out = context.stdout_capture.getvalue() - print out assert text in out @then('the journal should contain "{text}"') diff --git a/jrnl/Entry.py b/jrnl/Entry.py index a8ec0557..e8da761b 100644 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -15,7 +15,7 @@ class Entry: def parse_tags(self): fulltext = " ".join([self.title, self.body]).lower() - tags = re.findall(r'(?u)([{}]\w+)'.format(self.journal.config['tagsymbols']), fulltext, re.UNICODE) + tags = re.findall(r'(?u)([{tags}]\w+)'.format(tags=self.journal.config['tagsymbols']), fulltext, re.UNICODE) return set(tags) def __unicode__(self): diff --git a/jrnl/Journal.py b/jrnl/Journal.py index d4e733ea..60118184 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -152,7 +152,8 @@ class Journal(object): except ValueError: # Happens when we can't parse the start of the line as an date. # In this case, just append line to our body. - current_entry.body += line + "\n" + if current_entry: + current_entry.body += line + "\n" # Append last entry if current_entry: @@ -173,7 +174,7 @@ class Journal(object): lambda match: self._colorize(match.group(0)), pp, re.UNICODE) else: - pp = re.sub(r"(?u)([{}]\w+)".format(self.config['tagsymbols']), + pp = re.sub(r"(?u)([{tags}]\w+)".format(tags=self.config['tagsymbols']), lambda match: self._colorize(match.group(0)), pp) return pp From 7f680f626045b303f5d451e2e99618f5d419d0df Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 13:09:33 +0200 Subject: [PATCH 32/91] Python 3 compatibility for tests --- features/steps/core.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/features/steps/core.py b/features/steps/core.py index 4b57b47e..130fe5b4 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -3,7 +3,10 @@ from jrnl import jrnl import os import sys import json -import StringIO +try: + from io import StringIO +except ImportError: + from cStringIO import StringIO def read_journal(journal_name="default"): with open(jrnl.CONFIG_PATH) as config_file: @@ -22,7 +25,7 @@ def set_config(context, config_file): def run_with_input(context, command, inputs=None): text = inputs or context.text args = command.split()[1:] - buffer = StringIO.StringIO(text.strip()) + buffer = StringIO(text.strip()) jrnl.util.STDIN = buffer jrnl.cli(args) From c303ea84555e945fad5e25bd62bdb8c9a20fc951 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 19 Jul 2013 13:24:18 +0200 Subject: [PATCH 33/91] Python 3 improvements --- jrnl/Journal.py | 5 ++++- jrnl/exporters.py | 9 ++++++--- jrnl/jrnl.py | 4 ++-- jrnl/util.py | 7 ++++++- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 60118184..4c49ad39 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -179,13 +179,16 @@ class Journal(object): pp) return pp + def pprint(self): + return self.__unicode__() + def __repr__(self): return "".format(len(self.entries)) def write(self, filename=None): """Dumps the journal into the config file, overwriting it""" filename = filename or self.config['journal'] - journal = "\n".join([unicode(e) for e in self.entries]) + journal = "\n".join([e.__unicode__() for e in self.entries]) if self.config['encrypt']: journal = self._encrypt(journal) with open(filename, 'wb') as journal_file: diff --git a/jrnl/exporters.py b/jrnl/exporters.py index 5bd687cf..4e2a9492 100644 --- a/jrnl/exporters.py +++ b/jrnl/exporters.py @@ -7,6 +7,9 @@ try: from slugify import slugify except ImportError: import slugify try: import simplejson as json except ImportError: import json +try: from .util import u +except (SystemError, ValueError): from util import u + def get_tags_count(journal): """Returns a set of tuples (count, tag) for all tags present in the journal.""" @@ -60,7 +63,7 @@ def to_md(journal): def to_txt(journal): """Returns the complete text of the Journal.""" - return unicode(journal) + return journal.pprint() def export(journal, format, output=None): """Exports the journal to various formats. @@ -93,7 +96,7 @@ def export(journal, format, output=None): def write_files(journal, path, format): """Turns your journal into separate files for each entry. Format should be either json, md or txt.""" - make_filename = lambda entry: e.date.strftime("%C-%m-%d_{}.{}".format(slugify(unicode(e.title)), format)) + make_filename = lambda entry: e.date.strftime("%C-%m-%d_{}.{}".format(slugify(u(e.title)), format)) for e in journal.entries: full_path = os.path.join(path, make_filename(e)) if format == 'json': @@ -101,7 +104,7 @@ def write_files(journal, path, format): elif format == 'md': content = e.to_md() elif format == 'txt': - content = unicode(e) + content = u(e) with open(full_path, 'w') as f: f.write(content) return "[Journal exported individual files in {}]".format(path) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 3eba8700..37a310b7 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -169,7 +169,7 @@ def cli(manual_args=None): # Writing mode if mode_compose: raw = " ".join(args.text).strip() - if type(raw) is not unicode: + if util.PY2 and type(raw) is not unicode: raw = raw.decode(sys.getfilesystemencoding()) entry = journal.new_entry(raw, args.date) entry.starred = args.star @@ -183,7 +183,7 @@ def cli(manual_args=None): strict=args.strict, short=args.short) journal.limit(args.limit) - print(unicode(journal)) + print(journal.pprint()) # Various export modes elif args.tags: diff --git a/jrnl/util.py b/jrnl/util.py index 5f7f0c52..4279bab0 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -3,10 +3,15 @@ import sys import os from tzlocal import get_localzone +PY3 = sys.version_info[0] == 3 +PY2 = sys.version_info[0] == 2 + +def u(s): + """Mock unicode function for python 2 and 3 compatibility.""" + return s if PY3 else unicode(s, "unicode_escape") STDIN = sys.stdin STDOUT = sys.stdout - __cached_tz = None def py23_input(msg): From 91638fd4295105097a662c73de10e2495c366e12 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 10:11:37 +0200 Subject: [PATCH 34/91] Tests for multiple journals --- features/configs/multiple.json | 16 +++++++++++++ features/environment.py | 2 ++ features/journals/work.journal | 0 features/multiple_journals.feature | 36 ++++++++++++++++++++++++++++++ features/steps/core.py | 34 ++++++++++++++++++++++++++-- jrnl/Journal.py | 1 - 6 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 features/configs/multiple.json create mode 100644 features/journals/work.journal create mode 100644 features/multiple_journals.feature diff --git a/features/configs/multiple.json b/features/configs/multiple.json new file mode 100644 index 00000000..af7a3e15 --- /dev/null +++ b/features/configs/multiple.json @@ -0,0 +1,16 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": false, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "", + "journals": { + "default": "features/journals/simple.journal", + "work": "features/journals/work.journal", + "ideas": "features/journals/nothing.journal" + }, + "tagsymbols": "@" +} diff --git a/features/environment.py b/features/environment.py index f374c25a..59763616 100644 --- a/features/environment.py +++ b/features/environment.py @@ -17,6 +17,8 @@ def after_scenario(context, scenario): for folder in ("configs", "journals"): original = os.path.join("features", folder) backup = os.path.join("features", folder+"_backup") + for filename in os.listdir(original): + os.remove(os.path.join(original, filename)) for filename in os.listdir(backup): shutil.copy2(os.path.join(backup, filename), original) shutil.rmtree(backup) diff --git a/features/journals/work.journal b/features/journals/work.journal new file mode 100644 index 00000000..e69de29b diff --git a/features/multiple_journals.feature b/features/multiple_journals.feature new file mode 100644 index 00000000..fb026d2e --- /dev/null +++ b/features/multiple_journals.feature @@ -0,0 +1,36 @@ +Feature: Multiple journals + + Scenario: Loading a config with two journals + Given we use the config "multiple.json" + Then journal "default" should have 2 entries + and journal "work" should have 0 entries + + Scenario: Write to default config by default + Given we use the config "multiple.json" + When we run "jrnl this goes to default" + Then journal "default" should have 3 entries + and journal "work" should have 0 entries + + Scenario: Write to specified journal + Given we use the config "multiple.json" + When we run "jrnl work a long day in the office" + Then journal "default" should have 2 entries + and journal "work" should have 1 entry + + Scenario: Tell user which journal was used + Given we use the config "multiple.json" + When we run "jrnl work a long day in the office" + Then the output should contain "Entry added to work journal" + + Scenario: Write to specified journal with a timestamp + Given we use the config "multiple.json" + When we run "jrnl work 23 july 2012: a long day in the office" + Then journal "default" should have 2 entries + and journal "work" should have 1 entry + and journal "work" should contain "2012-07-23" + + Scenario: Create new journals as required + Given we use the config "multiple.json" + Then journal "ideas" should not exist + When we run "jrnl ideas 23 july 2012: sell my junk on ebay and make lots of money" + Then journal "ideas" should have 1 entry diff --git a/features/steps/core.py b/features/steps/core.py index 130fe5b4..36369edc 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -1,5 +1,5 @@ from behave import * -from jrnl import jrnl +from jrnl import jrnl, Journal import os import sys import json @@ -15,6 +15,16 @@ def read_journal(journal_name="default"): journal = journal_file.read() return journal +def open_journal(journal_name="default"): + with open(jrnl.CONFIG_PATH) as config_file: + config = json.load(config_file) + journals = config['journals'] + if type(journals) is dict: # We can override the default config on a by-journal basis + config['journal'] = journals.get(journal_name) + else: # But also just give them a string to point to the journal file + config['journal'] = journal + return Journal.Journal(**config) + @given('we use the config "{config_file}"') def set_config(context, config_file): full_path = os.path.join("features/configs", config_file) @@ -52,8 +62,28 @@ def check_output_inline(context, text): assert text in out @then('the journal should contain "{text}"') -@then('journal {journal_name} should contain "{text}"') +@then('journal "{journal_name}" should contain "{text}"') def check_journal_content(context, text, journal_name="default"): journal = read_journal(journal_name) assert text in journal +@then('journal "{journal_name}" should not exist') +def journal_doesnt_exist(context, journal_name="default"): + with open(jrnl.CONFIG_PATH) as config_file: + config = json.load(config_file) + journal_path = config['journals'][journal_name] + print journal_path + assert not os.path.exists(journal_path) + +@then('the journal should have {number:d} entries') +@then('the journal should have {number:d} entry') +@then('journal "{journal_name}" should have {number:d} entries') +@then('journal "{journal_name}" should have {number:d} entry') +def check_journal_content(context, number, journal_name="default"): + journal = open_journal(journal_name) + assert len(journal.entries) == number + +@then('fail') +def debug_fail(context): + assert False + diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 4c49ad39..9deb2a38 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -50,7 +50,6 @@ class Journal(object): 'linewrap': 80, } self.config.update(kwargs) - # Set up date parser consts = pdt.Constants(usePyICU=False) consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday From 7cf4fd701ea62843f3a755aecb090290c22b2ed3 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 12:04:01 +0200 Subject: [PATCH 35/91] Uses stderr for prompts instead stdout --- CHANGELOG.md | 4 ++++ jrnl/Journal.py | 10 +++++----- jrnl/__init__.py | 2 +- jrnl/jrnl.py | 21 +++++++++++---------- jrnl/util.py | 5 +++++ 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a44755dc..1d1fb676 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +### 1.3.2 + +* [Improved] Everything that is not direct output of jrnl will be written stderr to improve integration + ### 1.3.0 * [New] Export to multiple files diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 9deb2a38..827c9551 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -3,8 +3,8 @@ try: from . import Entry except (SystemError, ValueError): import Entry -try: from .util import get_local_timezone -except (SystemError, ValueError): from util import get_local_timezone +try: from .util import get_local_timezone, prompt +except (SystemError, ValueError): from util import get_local_timezone, prompt import codecs import os try: import parsedatetime.parsedatetime_consts as pdt @@ -76,7 +76,7 @@ class Journal(object): try: plain = crypto.decrypt(cipher[16:]) except ValueError: - print("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?") + prompt("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?") sys.exit(-1) if plain[-1] != " ": # Journals are always padded return None @@ -118,9 +118,9 @@ class Journal(object): attempts += 1 self.config['password'] = None # This password doesn't work. if attempts < 3: - print("Wrong password, try again.") + prompt("Wrong password, try again.") else: - print("Extremely wrong password.") + prompt("Extremely wrong password.") sys.exit(-1) journal = decrypted else: diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 7ca6b98e..d17a3b01 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.3.1' +__version__ = '1.3.2' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 37a310b7..ef78840b 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -29,6 +29,7 @@ xdg_config = os.environ.get('XDG_CONFIG_HOME') CONFIG_PATH = os.path.join(xdg_config, "jrnl") if xdg_config else os.path.expanduser('~/.jrnl_config') PYCRYPTO = install.module_exists("Crypto") + def parse_args(args=None): parser = argparse.ArgumentParser() composing = parser.add_argument_group('Composing', 'Will make an entry out of whatever follows as arguments') @@ -77,7 +78,7 @@ def get_text_from_editor(config): raw = f.read() os.remove(tmpfile) else: - print('[Nothing saved to file]') + util.prompt('[Nothing saved to file]') raw = '' return raw @@ -89,19 +90,19 @@ def encrypt(journal, filename=None): journal.make_key(prompt="Enter new password:") journal.config['encrypt'] = True journal.write(filename) - print("Journal encrypted to {0}.".format(filename or journal.config['journal'])) + util.prompt("Journal encrypted to {0}.".format(filename or journal.config['journal'])) def decrypt(journal, filename=None): """ Decrypts into new file. If filename is not set, we encrypt the journal file itself. """ journal.config['encrypt'] = False journal.config['password'] = "" journal.write(filename) - print("Journal decrypted to {0}.".format(filename or journal.config['journal'])) + util.prompt("Journal decrypted to {0}.".format(filename or journal.config['journal'])) def touch_journal(filename): """If filename does not exist, touch the file""" if not os.path.exists(filename): - print("[Journal created at {0}]".format(filename)) + util.prompt("[Journal created at {0}]".format(filename)) open(filename, 'a').close() def update_config(config, new_config, scope): @@ -122,15 +123,15 @@ def cli(manual_args=None): try: config = json.load(f) except ValueError as e: - print("[There seems to be something wrong with your jrnl config at {}: {}]".format(CONFIG_PATH, e.message)) - print("[Entry was NOT added to your journal]") + util.prompt("[There seems to be something wrong with your jrnl config at {}: {}]".format(CONFIG_PATH, e.message)) + util.prompt("[Entry was NOT added to your journal]") sys.exit(-1) install.update_config(config, config_path=CONFIG_PATH) original_config = config.copy() # check if the configuration is supported by available modules if config['encrypt'] and not PYCRYPTO: - print("According to your jrnl_conf, your journal is encrypted, however PyCrypto was not found. To open your journal, install the PyCrypto package from http://www.pycrypto.org.") + util.prompt("According to your jrnl_conf, your journal is encrypted, however PyCrypto was not found. To open your journal, install the PyCrypto package from http://www.pycrypto.org.") sys.exit(-1) args = parse_args(manual_args) @@ -173,7 +174,7 @@ def cli(manual_args=None): raw = raw.decode(sys.getfilesystemencoding()) entry = journal.new_entry(raw, args.date) entry.starred = args.star - print("[Entry added to {0} journal]".format(journal_name)) + util.prompt("[Entry added to {0} journal]".format(journal_name)) journal.write() # Reading mode @@ -193,7 +194,7 @@ def cli(manual_args=None): print(exporters.export(journal, args.export, args.output)) elif (args.encrypt is not False or args.decrypt is not False) and not PYCRYPTO: - print("PyCrypto not found. To encrypt or decrypt your journal, install the PyCrypto package from http://www.pycrypto.org.") + util.prompt("PyCrypto not found. To encrypt or decrypt your journal, install the PyCrypto package from http://www.pycrypto.org.") elif args.encrypt is not False: encrypt(journal, filename=args.encrypt) @@ -211,7 +212,7 @@ def cli(manual_args=None): elif args.delete_last: last_entry = journal.entries.pop() - print("[Deleted Entry:]") + util.prompt("[Deleted Entry:]") print(last_entry) journal.write() diff --git a/jrnl/util.py b/jrnl/util.py index 4279bab0..ac432fbf 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -11,9 +11,14 @@ def u(s): return s if PY3 else unicode(s, "unicode_escape") STDIN = sys.stdin +STDERR = sys.stderr STDOUT = sys.stdout __cached_tz = None +def prompt(msg): + """Prints a message to the std err stream defined in util.""" + print(msg, file=STDERR) + def py23_input(msg): STDOUT.write(msg) return STDIN.readline().strip() From 0b7b88dcf07bfd6df4a5ce2fabb2a134d59d79d5 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 12:04:32 +0200 Subject: [PATCH 36/91] Tests for using stderr prompts --- features/core.feature | 4 ++-- features/environment.py | 9 +++++++++ features/multiple_journals.feature | 2 +- features/steps/core.py | 6 +++++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/features/core.feature b/features/core.feature index 7e495ef9..d5a6fa7f 100644 --- a/features/core.feature +++ b/features/core.feature @@ -16,14 +16,14 @@ Feature: Basic reading and writing to a journal Scenario: Writing an entry from command line Given we use the config "basic.json" When we run "jrnl 23 july 2013: A cold and stormy day. I ate crisps on the sofa." - Then the output should contain "Entry added" + Then we should see the message "Entry added" When we run "jrnl -n 1" Then the output should contain "2013-07-23 09:00 A cold and stormy day." Scenario: Emoji support Given we use the config "basic.json" When we run "jrnl 23 july 2013: 🌞 sunny day. Saw an 🐘" - Then the output should contain "Entry added" + Then we should see the message "Entry added" When we run "jrnl -n 1" Then the output should contain "🌞" and the output should contain "🐘" diff --git a/features/environment.py b/features/environment.py index 59763616..bebc2150 100644 --- a/features/environment.py +++ b/features/environment.py @@ -1,9 +1,16 @@ from behave import * import shutil import os +from jrnl import jrnl +try: + from io import StringIO +except ImportError: + from cStringIO import StringIO def before_scenario(context, scenario): """Before each scenario, backup all config and journal test data.""" + context.messages = StringIO() + jrnl.util.STDERR = context.messages for folder in ("configs", "journals"): original = os.path.join("features", folder) backup = os.path.join("features", folder+"_backup") @@ -14,6 +21,8 @@ def before_scenario(context, scenario): def after_scenario(context, scenario): """After each scenario, restore all test data and remove backups.""" + context.messages.close() + context.messages = None for folder in ("configs", "journals"): original = os.path.join("features", folder) backup = os.path.join("features", folder+"_backup") diff --git a/features/multiple_journals.feature b/features/multiple_journals.feature index fb026d2e..0510209b 100644 --- a/features/multiple_journals.feature +++ b/features/multiple_journals.feature @@ -20,7 +20,7 @@ Feature: Multiple journals Scenario: Tell user which journal was used Given we use the config "multiple.json" When we run "jrnl work a long day in the office" - Then the output should contain "Entry added to work journal" + Then we should see the message "Entry added to work journal" Scenario: Write to specified journal with a timestamp Given we use the config "multiple.json" diff --git a/features/steps/core.py b/features/steps/core.py index 36369edc..bab0ee96 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -61,6 +61,11 @@ def check_output_inline(context, text): out = context.stdout_capture.getvalue() assert text in out +@then('we should see the message "{text}"') +def check_message(context, text): + out = context.messages.getvalue() + assert text in out + @then('the journal should contain "{text}"') @then('journal "{journal_name}" should contain "{text}"') def check_journal_content(context, text, journal_name="default"): @@ -72,7 +77,6 @@ def journal_doesnt_exist(context, journal_name="default"): with open(jrnl.CONFIG_PATH) as config_file: config = json.load(config_file) journal_path = config['journals'][journal_name] - print journal_path assert not os.path.exists(journal_path) @then('the journal should have {number:d} entries') From 95f68a5109f9789e8ad21d456cc5b885cc896d18 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 20:08:41 +0200 Subject: [PATCH 37/91] Allows getpass to get bypassed by reading from stdin --- jrnl/Journal.py | 9 +++++---- jrnl/util.py | 25 ++++++++++++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 827c9551..43d1d2e6 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -3,8 +3,8 @@ try: from . import Entry except (SystemError, ValueError): import Entry -try: from .util import get_local_timezone, prompt -except (SystemError, ValueError): from util import get_local_timezone, prompt +try: from .util import get_local_timezone, prompt, getpass +except (SystemError, ValueError): from util import get_local_timezone, prompt, getpass import codecs import os try: import parsedatetime.parsedatetime_consts as pdt @@ -25,7 +25,6 @@ except ImportError: if "win32" in sys.platform: import pyreadline as readline else: import readline import hashlib -import getpass try: import colorama colorama.init() @@ -89,6 +88,7 @@ class Journal(object): sys.exit("Error: PyCrypto is not installed.") atfork() # A seed for PyCrypto iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16)) + print("iv", iv, len(iv)) crypto = AES.new(self.key, AES.MODE_CBC, iv) if len(plain) % 16 != 0: plain += " " * (16 - len(plain) % 16) @@ -98,7 +98,8 @@ class Journal(object): def make_key(self, prompt="Password: "): """Creates an encryption key from the default password or prompts for a new password.""" - password = self.config['password'] or getpass.getpass(prompt) + password = self.config['password'] or getpass(prompt) + print("GOT PWD", password) self.key = hashlib.sha256(password.encode('utf-8')).digest() def open(self, filename=None): diff --git a/jrnl/util.py b/jrnl/util.py index ac432fbf..54a60c66 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -3,24 +3,35 @@ import sys import os from tzlocal import get_localzone +import getpass as gp + PY3 = sys.version_info[0] == 3 PY2 = sys.version_info[0] == 2 +STDIN = sys.stdin +STDERR = sys.stderr +STDOUT = sys.stdout +TEST = False +__cached_tz = None + +def getpass(prompt): + if not TEST: + return gp.getpass(prompt) + else: + return py23_input(prompt) + def u(s): """Mock unicode function for python 2 and 3 compatibility.""" return s if PY3 else unicode(s, "unicode_escape") -STDIN = sys.stdin -STDERR = sys.stderr -STDOUT = sys.stdout -__cached_tz = None - def prompt(msg): """Prints a message to the std err stream defined in util.""" - print(msg, file=STDERR) + if not msg.endswith("\n"): + msg += "\n" + STDERR.write(u(msg)) def py23_input(msg): - STDOUT.write(msg) + STDERR.write(u(msg)) return STDIN.readline().strip() def get_local_timezone(): From b1266dcc4e89651350d208880ffa108571e33648 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 20:08:53 +0200 Subject: [PATCH 38/91] Tests for encryption --- features/configs/encrypted.json | 14 ++++++++++++++ features/encryption.feature | 24 ++++++++++++++++++++++++ features/environment.py | 1 + features/journals/encrypted.journal | 3 +++ features/steps/core.py | 14 ++++++++++++++ 5 files changed, 56 insertions(+) create mode 100644 features/configs/encrypted.json create mode 100644 features/encryption.feature create mode 100644 features/journals/encrypted.journal diff --git a/features/configs/encrypted.json b/features/configs/encrypted.json new file mode 100644 index 00000000..a498974b --- /dev/null +++ b/features/configs/encrypted.json @@ -0,0 +1,14 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": true, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "", + "journals": { + "default": "features/journals/encrypted.journal" + }, + "tagsymbols": "@" +} diff --git a/features/encryption.feature b/features/encryption.feature new file mode 100644 index 00000000..c7f94d62 --- /dev/null +++ b/features/encryption.feature @@ -0,0 +1,24 @@ + Feature: Multiple journals + + Scenario: Loading an encrypted journal + Given we use the config "encrypted.json" + When we run "jrnl -n 1" and enter "bad doggie no biscuit" + Then we should see the message "Password" + and the output should contain "2013-06-10 15:40 Life is good" + + Scenario: Decrypting a journal + Given we use the config "encrypted.json" + When we run "jrnl --decrypt" and enter "bad doggie no biscuit" + Then we should see the message "Journal decrypted" + and the journal should have 2 entries + and the config should have "encrypt" set to "bool:False" + + Scenario: Encrypting a journal + Given we use the config "basic.json" + When we run "jrnl --encrypt" and enter "swordfish" + Then we should see the message "Journal encrypted" + and the config should have "encrypt" set to "bool:True" + When we run "jrnl -n 1" and enter "swordish" + Then we should see the message "Password" + and the output should contain "2013-06-10 15:40 Life is good" + diff --git a/features/environment.py b/features/environment.py index bebc2150..a25d2fff 100644 --- a/features/environment.py +++ b/features/environment.py @@ -11,6 +11,7 @@ def before_scenario(context, scenario): """Before each scenario, backup all config and journal test data.""" context.messages = StringIO() jrnl.util.STDERR = context.messages + jrnl.util.TEST = True for folder in ("configs", "journals"): original = os.path.join("features", folder) backup = os.path.join("features", folder+"_backup") diff --git a/features/journals/encrypted.journal b/features/journals/encrypted.journal new file mode 100644 index 00000000..1c40a799 --- /dev/null +++ b/features/journals/encrypted.journal @@ -0,0 +1,3 @@ +~|5\< +hqFCZ[\ELxy +eowW( O4;p[fD$K7 4C{&;duj|Z@?WGݕW ,z2 \ No newline at end of file diff --git a/features/steps/core.py b/features/steps/core.py index bab0ee96..531410aa 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -79,6 +79,20 @@ def journal_doesnt_exist(context, journal_name="default"): journal_path = config['journals'][journal_name] assert not os.path.exists(journal_path) +@then('the config should have "{key}" set to "{value}"') +def config_var(context, key, value): + t, value = value.split(":") + value = { + "bool": lambda v: v.lower() == "true", + "int": int, + "str": str + }[t](value) + with open(jrnl.CONFIG_PATH) as config_file: + config = json.load(config_file) + assert key in config + print key, config[key], type(config[key]), value, type(value) + assert config[key] == value + @then('the journal should have {number:d} entries') @then('the journal should have {number:d} entry') @then('journal "{journal_name}" should have {number:d} entries') From 9c3841dded69ba78cd30341873e31c5bdddf56ab Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 21:19:30 +0200 Subject: [PATCH 39/91] Changes cleaning strategy --- features/{ => data}/configs/basic.json | 0 features/{ => data}/configs/encrypted.json | 0 features/{ => data}/configs/multiple.json | 0 features/data/journals/encrypted.journal | Bin 0 -> 128 bytes features/{ => data}/journals/simple.journal | 0 features/{ => data}/journals/work.journal | 0 features/environment.py | 21 ++++++++------------ features/journals/encrypted.journal | 3 --- 8 files changed, 8 insertions(+), 16 deletions(-) rename features/{ => data}/configs/basic.json (100%) rename features/{ => data}/configs/encrypted.json (100%) rename features/{ => data}/configs/multiple.json (100%) create mode 100644 features/data/journals/encrypted.journal rename features/{ => data}/journals/simple.journal (100%) rename features/{ => data}/journals/work.journal (100%) delete mode 100644 features/journals/encrypted.journal diff --git a/features/configs/basic.json b/features/data/configs/basic.json similarity index 100% rename from features/configs/basic.json rename to features/data/configs/basic.json diff --git a/features/configs/encrypted.json b/features/data/configs/encrypted.json similarity index 100% rename from features/configs/encrypted.json rename to features/data/configs/encrypted.json diff --git a/features/configs/multiple.json b/features/data/configs/multiple.json similarity index 100% rename from features/configs/multiple.json rename to features/data/configs/multiple.json diff --git a/features/data/journals/encrypted.journal b/features/data/journals/encrypted.journal new file mode 100644 index 0000000000000000000000000000000000000000..339b47baf9671f4550efeb9b6a0cfcd5032255d6 GIT binary patch literal 128 zcmV-`0Du3(bJIGVsY(mXmoW-2hF&*L`0NbJTYlTUr8*^Qm97}8E^3^1bZ$P^M literal 0 HcmV?d00001 diff --git a/features/journals/simple.journal b/features/data/journals/simple.journal similarity index 100% rename from features/journals/simple.journal rename to features/data/journals/simple.journal diff --git a/features/journals/work.journal b/features/data/journals/work.journal similarity index 100% rename from features/journals/work.journal rename to features/data/journals/work.journal diff --git a/features/environment.py b/features/environment.py index a25d2fff..89125fca 100644 --- a/features/environment.py +++ b/features/environment.py @@ -13,22 +13,17 @@ def before_scenario(context, scenario): jrnl.util.STDERR = context.messages jrnl.util.TEST = True for folder in ("configs", "journals"): - original = os.path.join("features", folder) - backup = os.path.join("features", folder+"_backup") - if not os.path.exists(backup): - os.mkdir(backup) + original = os.path.join("features", "data", folder) + working_dir = os.path.join("features", folder) + if not os.path.exists(working_dir): + os.mkdir(working_dir) for filename in os.listdir(original): - shutil.copy2(os.path.join(original, filename), backup) + shutil.copy2(os.path.join(original, filename), working_dir) def after_scenario(context, scenario): - """After each scenario, restore all test data and remove backups.""" + """After each scenario, restore all test data and remove working_dirs.""" context.messages.close() context.messages = None for folder in ("configs", "journals"): - original = os.path.join("features", folder) - backup = os.path.join("features", folder+"_backup") - for filename in os.listdir(original): - os.remove(os.path.join(original, filename)) - for filename in os.listdir(backup): - shutil.copy2(os.path.join(backup, filename), original) - shutil.rmtree(backup) + working_dir = os.path.join("features", folder) + shutil.rmtree(working_dir) diff --git a/features/journals/encrypted.journal b/features/journals/encrypted.journal deleted file mode 100644 index 1c40a799..00000000 --- a/features/journals/encrypted.journal +++ /dev/null @@ -1,3 +0,0 @@ -~|5\< -hqFCZ[\ELxy -eowW( O4;p[fD$K7 4C{&;duj|Z@?WGݕW ,z2 \ No newline at end of file From e78786e19a63e2962c0dc994fed62d74f2a0c5a6 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 21:24:19 +0200 Subject: [PATCH 40/91] Unifies encryption between python versions --- CHANGELOG.md | 4 ++++ jrnl/Journal.py | 37 +++++++++++++++++-------------------- jrnl/__init__.py | 2 +- jrnl/util.py | 2 +- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d1fb676..e27814bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +### 1.4.0 + +* [Improved] Unifies encryption between Python 2 and 3. If you have problems reading encrypted journals afterwards, first decrypt your journal with the __old__ jrnl version (install with `pip install jrnl==1.3.1`, then `jrnl --decrypt`), upgrade jrnl (`pip install jrnl --upgrade`) and encrypt it again (`jrnl --encrypt`). + ### 1.3.2 * [Improved] Everything that is not direct output of jrnl will be written stderr to improve integration diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 43d1d2e6..7c680b21 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -3,8 +3,8 @@ try: from . import Entry except (SystemError, ValueError): import Entry -try: from .util import get_local_timezone, prompt, getpass -except (SystemError, ValueError): from util import get_local_timezone, prompt, getpass +try: from . import util +except (SystemError, ValueError): import util import codecs import os try: import parsedatetime.parsedatetime_consts as pdt @@ -18,7 +18,7 @@ import sys import glob try: from Crypto.Cipher import AES - from Crypto.Random import random, atfork + from Crypto import Random crypto_installed = True except ImportError: crypto_installed = False @@ -75,32 +75,29 @@ class Journal(object): try: plain = crypto.decrypt(cipher[16:]) except ValueError: - prompt("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?") + util.prompt("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?") sys.exit(-1) - if plain[-1] != " ": # Journals are always padded + padding = " ".encode("utf-8") + if not plain.endswith(padding): # Journals are always padded return None else: - return plain + return plain.decode("utf-8") def _encrypt(self, plain): """Encrypt a plaintext string using self.key as the key""" if not crypto_installed: sys.exit("Error: PyCrypto is not installed.") - atfork() # A seed for PyCrypto - iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16)) - print("iv", iv, len(iv)) + Random.atfork() # A seed for PyCrypto + iv = Random.new().read(AES.block_size) crypto = AES.new(self.key, AES.MODE_CBC, iv) - if len(plain) % 16 != 0: - plain += " " * (16 - len(plain) % 16) - else: # Always pad so we can detect properly decrypted files :) - plain += " " * 16 + plain = plain.encode("utf-8") + plain += b" " * (AES.block_size - len(plain) % AES.block_size) return iv + crypto.encrypt(plain) def make_key(self, prompt="Password: "): """Creates an encryption key from the default password or prompts for a new password.""" - password = self.config['password'] or getpass(prompt) - print("GOT PWD", password) - self.key = hashlib.sha256(password.encode('utf-8')).digest() + password = self.config['password'] or util.getpass(prompt) + self.key = hashlib.sha256(password.encode("utf-8")).digest() def open(self, filename=None): """Opens the journal file defined in the config and parses it into a list of Entries. @@ -119,9 +116,9 @@ class Journal(object): attempts += 1 self.config['password'] = None # This password doesn't work. if attempts < 3: - prompt("Wrong password, try again.") + util.prompt("Wrong password, try again.") else: - prompt("Extremely wrong password.") + util.prompt("Extremely wrong password.") sys.exit(-1) journal = decrypted else: @@ -318,7 +315,7 @@ class DayOne(Journal): try: timezone = pytz.timezone(dict_entry['Time Zone']) except pytz.exceptions.UnknownTimeZoneError: - timezone = pytz.timezone(get_local_timezone()) + timezone = pytz.timezone(util.get_local_timezone()) date = dict_entry['Creation Date'] date = date + timezone.utcoffset(date) entry = self.new_entry(raw=dict_entry['Entry Text'], date=date, sort=False) @@ -344,7 +341,7 @@ class DayOne(Journal): 'Creation Date': utc_time, 'Starred': entry.starred if hasattr(entry, 'starred') else False, 'Entry Text': entry.title+"\n"+entry.body, - 'Time Zone': get_local_timezone(), + 'Time Zone': util.get_local_timezone(), 'UUID': new_uuid } plistlib.writePlist(entry_plist, filename) diff --git a/jrnl/__init__.py b/jrnl/__init__.py index d17a3b01..d41e846b 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.3.2' +__version__ = '1.4.0' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' diff --git a/jrnl/util.py b/jrnl/util.py index 54a60c66..28499933 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -22,7 +22,7 @@ def getpass(prompt): def u(s): """Mock unicode function for python 2 and 3 compatibility.""" - return s if PY3 else unicode(s, "unicode_escape") + return s if PY3 or type(s) is unicode else unicode(s, "unicode_escape") def prompt(msg): """Prints a message to the std err stream defined in util.""" From bf69e0042e54baf741023cde641d3fd1067e6ad2 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 21:24:25 +0200 Subject: [PATCH 41/91] Fixes encryption tests --- features/encryption.feature | 2 +- features/steps/core.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/features/encryption.feature b/features/encryption.feature index c7f94d62..a18e2477 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -18,7 +18,7 @@ When we run "jrnl --encrypt" and enter "swordfish" Then we should see the message "Journal encrypted" and the config should have "encrypt" set to "bool:True" - When we run "jrnl -n 1" and enter "swordish" + When we run "jrnl -n 1" and enter "swordfish" Then we should see the message "Password" and the output should contain "2013-06-10 15:40 Life is good" diff --git a/features/steps/core.py b/features/steps/core.py index 531410aa..b3675c1c 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -90,7 +90,6 @@ def config_var(context, key, value): with open(jrnl.CONFIG_PATH) as config_file: config = json.load(config_file) assert key in config - print key, config[key], type(config[key]), value, type(value) assert config[key] == value @then('the journal should have {number:d} entries') From 601e574d91d01a61dd933cbf546a649e13bbb134 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 22 Jul 2013 21:26:21 +0200 Subject: [PATCH 42/91] Test for decrypting journals when password is saved in config --- features/data/configs/encrypted_with_pw.json | 14 ++++++++++++++ features/encryption.feature | 5 +++++ 2 files changed, 19 insertions(+) create mode 100644 features/data/configs/encrypted_with_pw.json diff --git a/features/data/configs/encrypted_with_pw.json b/features/data/configs/encrypted_with_pw.json new file mode 100644 index 00000000..1a277240 --- /dev/null +++ b/features/data/configs/encrypted_with_pw.json @@ -0,0 +1,14 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": true, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "bad doggie no biscuit", + "journals": { + "default": "features/journals/encrypted.journal" + }, + "tagsymbols": "@" +} diff --git a/features/encryption.feature b/features/encryption.feature index a18e2477..d134c3bb 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -6,6 +6,11 @@ Then we should see the message "Password" and the output should contain "2013-06-10 15:40 Life is good" + Scenario: Loading an encrypted journal with password in config + Given we use the config "encrypted_with_pw.json" + When we run "jrnl -n 1" + Then the output should contain "2013-06-10 15:40 Life is good" + Scenario: Decrypting a journal Given we use the config "encrypted.json" When we run "jrnl --decrypt" and enter "bad doggie no biscuit" From 13a0e7ce86067246dfb9ce2af5ab719e5f686f11 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Tue, 23 Jul 2013 21:01:57 -0700 Subject: [PATCH 43/91] Tagging fixes --- CHANGELOG.md | 4 ++++ jrnl/Entry.py | 1 + jrnl/__init__.py | 2 +- jrnl/exporters.py | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e27814bf..e9563f6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +### 1.4.1 + +* [Fixed] Tagging works again + ### 1.4.0 * [Improved] Unifies encryption between Python 2 and 3. If you have problems reading encrypted journals afterwards, first decrypt your journal with the __old__ jrnl version (install with `pip install jrnl==1.3.1`, then `jrnl --decrypt`), upgrade jrnl (`pip install jrnl --upgrade`) and encrypt it again (`jrnl --encrypt`). diff --git a/jrnl/Entry.py b/jrnl/Entry.py index e8da761b..5fbdeb15 100644 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -16,6 +16,7 @@ class Entry: def parse_tags(self): fulltext = " ".join([self.title, self.body]).lower() tags = re.findall(r'(?u)([{tags}]\w+)'.format(tags=self.journal.config['tagsymbols']), fulltext, re.UNICODE) + self.tags = tags return set(tags) def __unicode__(self): diff --git a/jrnl/__init__.py b/jrnl/__init__.py index d41e846b..e6c3253f 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.4.0' +__version__ = '1.4.1' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' diff --git a/jrnl/exporters.py b/jrnl/exporters.py index 4e2a9492..42126802 100644 --- a/jrnl/exporters.py +++ b/jrnl/exporters.py @@ -32,7 +32,7 @@ def to_tag_list(journal): elif min(tag_counts)[0] == 0: tag_counts = filter(lambda x: x[0] > 1, tag_counts) result += '[Removed tags that appear only once.]\n' - result += "\n".join(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=False)) + result += "\n".join(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)) return result def to_json(journal): From 6bcf83ed787854ac17fb048dc732d12d546c63ef Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Tue, 23 Jul 2013 21:02:03 -0700 Subject: [PATCH 44/91] Tests for tagging --- features/data/configs/tags.json | 14 ++++++++++++++ features/data/journals/tags.journal | 7 +++++++ features/tagging.feature | 12 ++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 features/data/configs/tags.json create mode 100644 features/data/journals/tags.journal create mode 100644 features/tagging.feature diff --git a/features/data/configs/tags.json b/features/data/configs/tags.json new file mode 100644 index 00000000..dc69950c --- /dev/null +++ b/features/data/configs/tags.json @@ -0,0 +1,14 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": false, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "", + "journals": { + "default": "features/journals/tags.journal" + }, + "tagsymbols": "@" +} diff --git a/features/data/journals/tags.journal b/features/data/journals/tags.journal new file mode 100644 index 00000000..7b5cdf04 --- /dev/null +++ b/features/data/journals/tags.journal @@ -0,0 +1,7 @@ +2013-06-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. diff --git a/features/tagging.feature b/features/tagging.feature new file mode 100644 index 00000000..a030d610 --- /dev/null +++ b/features/tagging.feature @@ -0,0 +1,12 @@ +Feature: Tagging + + Scenario: Displaying tags + Given we use the config "tags.json" + When we run "jrnl --tags" + Then we should get no error + and the output should be + """ + @idea : 2 + @journal : 1 + @dan : 1 + """ From 08d6f5fa0642fa0198c79e408844a74f96063d4a Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 24 Jul 2013 15:03:54 -0700 Subject: [PATCH 45/91] Links for getting help --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 58a03d6d..71fe1716 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ jrnl [![Build Status](https://travis-ci.org/maebert/jrnl.png?branch=master)](https://travis-ci.org/maebert/jrnl) ==== +_For news on updates or to get help, follow [@maebert](https://twitter.com/maebert) or [submit an issue](https://github.com/maebert/jrnl/issues/new) on Github._ + *jrnl* is a simple journal application for your command line. Journals are stored as human readable plain text files - you can put them into a Dropbox folder for instant syncing and you can be assured that your journal will still be readable in 2050, when all your fancy iPad journal applications will long be forgotten. *jrnl* also plays nice with the fabulous [DayOne](http://dayoneapp.com/) and can read and write directly from and to DayOne Journals. From 9dda1b39bdffc55ad8c1678313940af12cc9dec3 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 26 Jul 2013 19:40:39 -0700 Subject: [PATCH 46/91] Updates distribution info --- CHANGELOG.md | 3 ++- jrnl/__init__.py | 2 +- setup.py | 6 ++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9563f6a..6c3c430c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ Changelog ========= -### 1.4.1 +### 1.4.2 * [Fixed] Tagging works again +* Meta-info for PyPi updated ### 1.4.0 diff --git a/jrnl/__init__.py b/jrnl/__init__.py index e6c3253f..64f3178f 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.4.1' +__version__ = '1.4.2' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' diff --git a/setup.py b/setup.py index c6a53760..7f7e5d6c 100644 --- a/setup.py +++ b/setup.py @@ -92,14 +92,16 @@ setup( 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.3', 'Topic :: Office/Business :: News/Diary', 'Topic :: Text Processing' ], # metadata for upload to PyPI author = "Manuel Ebert", - author_email = "manuel@hey.com", + author_email = "manuel@1450.me", license = "MIT License", keywords = "journal todo todo.txt jrnl".split(), - url = "http://maebert.github.com/jrnl", + url = "http://maebert.github.io/jrnl", ) From 978d9db07285772e6bf0acaa41e70f33276b287c Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Tue, 6 Aug 2013 17:57:21 -0700 Subject: [PATCH 47/91] Filters before exporting --- jrnl/Journal.py | 1 + jrnl/jrnl.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 7c680b21..9a7767e8 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -54,6 +54,7 @@ class Journal(object): consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday self.dateparse = pdt.Calendar(consts) self.key = None # used to decrypt and encrypt the journal + self.search_tags = None # Store tags we're highlighting journal_txt = self.open() self.entries = self.parse(journal_txt) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index ef78840b..256075ce 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -176,14 +176,15 @@ def cli(manual_args=None): entry.starred = args.star util.prompt("[Entry added to {0} journal]".format(journal_name)) journal.write() - - # Reading mode - elif not mode_export: + else: journal.filter(tags=args.text, start_date=args.start_date, end_date=args.end_date, strict=args.strict, short=args.short) journal.limit(args.limit) + + # Reading mode + if not mode_export: print(journal.pprint()) # Various export modes From 3ef6095b411480aec11c8622a82fec380dfa9d5c Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Tue, 6 Aug 2013 17:57:31 -0700 Subject: [PATCH 48/91] Tests for filtering --- features/data/journals/tags.journal | 2 +- features/exporting.feature | 52 +++++++++++++++++++++++++++++ features/steps/core.py | 18 ++++++++-- features/tagging.feature | 10 ++++++ 4 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 features/exporting.feature diff --git a/features/data/journals/tags.journal b/features/data/journals/tags.journal index 7b5cdf04..bfd49cf4 100644 --- a/features/data/journals/tags.journal +++ b/features/data/journals/tags.journal @@ -1,4 +1,4 @@ -2013-06-09 15:39 I have an @idea: +2013-04-09 15:39 I have an @idea: (1) write a command line @journal software (2) ??? (3) PROFIT! diff --git a/features/exporting.feature b/features/exporting.feature new file mode 100644 index 00000000..26a0f7b4 --- /dev/null +++ b/features/exporting.feature @@ -0,0 +1,52 @@ +Feature: Expoting a Journal + + Scenario: Exporting to json + Given we use the config "tags.json" + When we run "jrnl --export json" + Then we should get no error + and the output should be + """ + { + "entries": [ + { + "body": "(1) write a command line @journal software\n(2) ???\n(3) PROFIT!", + "date": "2013-04-09", + "time": "15:39", + "title": "I have an @idea:" + }, + { + "body": "As alway's he shared his latest @idea on how to rule the world with me.", + "date": "2013-06-10", + "time": "15:40", + "title": "I met with @dan." + } + ], + "tags": { + "@idea": 2, + "@journal": 1, + "@dan": 1 + } + } + """ + + Scenario: Exporting using filters should only export parts of the journal + Given we use the config "tags.json" + When we run "jrnl -to 'may 2013' --export json" + Then we should get no error + and the output should be + """ + { + "entries": [ + { + "body": "(1) write a command line @journal software\n(2) ???\n(3) PROFIT!", + "date": "2013-04-09", + "time": "15:39", + "title": "I have an @idea:" + } + ], + "tags": { + "@idea": 1, + "@journal": 1 + } + } + """ diff --git a/features/steps/core.py b/features/steps/core.py index b3675c1c..fc9e15ff 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -8,6 +8,20 @@ try: except ImportError: from cStringIO import StringIO +def _parse_args(command): + nargs=[] + concats = [] + for a in command.split()[1:]: + if a.startswith("'"): + concats.append(a.strip("'")) + elif a.endswith("'"): + concats.append(a.strip("'")) + nargs.append(u" ".join(concats)) + concats = [] + else: + nargs.append(a) + return nargs + def read_journal(journal_name="default"): with open(jrnl.CONFIG_PATH) as config_file: config = json.load(config_file) @@ -34,14 +48,14 @@ def set_config(context, config_file): @when('we run "{command}" and enter "{inputs}"') def run_with_input(context, command, inputs=None): text = inputs or context.text - args = command.split()[1:] + args = _parse_args(command) buffer = StringIO(text.strip()) jrnl.util.STDIN = buffer jrnl.cli(args) @when('we run "{command}"') def run(context, command): - args = command.split()[1:] + args = _parse_args(command) jrnl.cli(args or None) diff --git a/features/tagging.feature b/features/tagging.feature index a030d610..a30c1052 100644 --- a/features/tagging.feature +++ b/features/tagging.feature @@ -10,3 +10,13 @@ Feature: Tagging @journal : 1 @dan : 1 """ + + Scenario: Filtering journals should also filter tags + Given we use the config "tags.json" + When we run "jrnl -from 'may 2013' --tags" + Then we should get no error + and the output should be + """ + @idea : 1 + @dan : 1 + """ From 1889cee6c3acf58b9143226d1ad4521ccbaebec3 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Tue, 6 Aug 2013 17:57:36 -0700 Subject: [PATCH 49/91] Version bump --- CHANGELOG.md | 8 ++++++-- jrnl/__init__.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c3c430c..ca77eb20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ Changelog ========= -### 1.4.2 +### 1.5.0 + +* [Improved] Exporting, encrypting and displaying tags now takes your filter options into account. So you could export everything before May 2012: `jrnl -to 'may 2012' --export json`. Or encrypt all entries tagged with `@work` into a new journal: `jrnl @work --encrypt work_journal.txt`. Or display all tags of posts where Bob is also tagged: `jrnl @bob --tags` + +#### 1.4.2 * [Fixed] Tagging works again * Meta-info for PyPi updated @@ -10,7 +14,7 @@ Changelog * [Improved] Unifies encryption between Python 2 and 3. If you have problems reading encrypted journals afterwards, first decrypt your journal with the __old__ jrnl version (install with `pip install jrnl==1.3.1`, then `jrnl --decrypt`), upgrade jrnl (`pip install jrnl --upgrade`) and encrypt it again (`jrnl --encrypt`). -### 1.3.2 +#### 1.3.2 * [Improved] Everything that is not direct output of jrnl will be written stderr to improve integration diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 64f3178f..6c783c61 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.4.2' +__version__ = '1.5.0' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From ff0b85e28b2506245b24417f0c924038a0c1a6cb Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Tue, 6 Aug 2013 18:10:42 -0700 Subject: [PATCH 50/91] Better tests for exporting to json --- features/exporting.feature | 54 +++++++++----------------------------- features/steps/core.py | 27 +++++++++++++++++++ 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/features/exporting.feature b/features/exporting.feature index 26a0f7b4..7f58876f 100644 --- a/features/exporting.feature +++ b/features/exporting.feature @@ -4,49 +4,19 @@ Feature: Expoting a Journal Given we use the config "tags.json" When we run "jrnl --export json" Then we should get no error - and the output should be - """ - { - "entries": [ - { - "body": "(1) write a command line @journal software\n(2) ???\n(3) PROFIT!", - "date": "2013-04-09", - "time": "15:39", - "title": "I have an @idea:" - }, - { - "body": "As alway's he shared his latest @idea on how to rule the world with me.", - "date": "2013-06-10", - "time": "15:40", - "title": "I met with @dan." - } - ], - "tags": { - "@idea": 2, - "@journal": 1, - "@dan": 1 - } - } - """ + and the output should be parsable as json + 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 "@journal" + and "tags" in the json output should contain "@dan" Scenario: Exporting using filters should only export parts of the journal Given we use the config "tags.json" When we run "jrnl -to 'may 2013' --export json" - Then we should get no error - and the output should be - """ - { - "entries": [ - { - "body": "(1) write a command line @journal software\n(2) ???\n(3) PROFIT!", - "date": "2013-04-09", - "time": "15:39", - "title": "I have an @idea:" - } - ], - "tags": { - "@idea": 1, - "@journal": 1 - } - } - """ + # Then we should get no error + Then the output should be parsable as json + 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 "@journal" + and "tags" in the json output should not contain "@dan" + diff --git a/features/steps/core.py b/features/steps/core.py index fc9e15ff..bceab051 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -63,6 +63,33 @@ def run(context, command): def no_error(context): assert context.failed is False +@then('the output should be parsable as json') +def check_output_json(context): + out = context.stdout_capture.getvalue() + assert json.loads(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 + assert len(out_json[field]) == number + +@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 output should be') def check_output(context): text = context.text.strip().splitlines() From 0e2f63a5b1d03e28660e9805ac7df8b00f82571b Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Fri, 9 Aug 2013 11:45:23 -0700 Subject: [PATCH 51/91] Hello 2013 :fireworks: --- LICENSE | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 4d488745..df698485 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,9 @@ -Copyright (c) 2012 Manuel Ebert +Copyright (c) 2013 Manuel Ebert Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +:f From 619285fb7fd95912dd1840bca55043b8bb9258c4 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 14 Aug 2013 14:00:32 -0700 Subject: [PATCH 52/91] Prevents journal from printing when composing an entry This fixes #87 --- jrnl/jrnl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 256075ce..5763a90d 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -184,7 +184,7 @@ def cli(manual_args=None): journal.limit(args.limit) # Reading mode - if not mode_export: + if not mode_compose and not mode_export: print(journal.pprint()) # Various export modes From ad98031c7f94fbf72bdec7929b671e47112efad2 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 14 Aug 2013 14:00:42 -0700 Subject: [PATCH 53/91] Adds regression test for #87 --- features/regression.feature | 8 ++++++++ features/steps/core.py | 5 +++++ 2 files changed, 13 insertions(+) create mode 100644 features/regression.feature diff --git a/features/regression.feature b/features/regression.feature new file mode 100644 index 00000000..c4cf41e7 --- /dev/null +++ b/features/regression.feature @@ -0,0 +1,8 @@ +Feature: Zapped bugs should stay dead. + + Scenario: Writing an entry does not print the entire journal + Given we use the config "basic.json" + When we run "jrnl 23 july 2013: A cold and stormy day. I ate crisps on the sofa." + Then we should see the message "Entry added" + When we run "jrnl -n 1" + Then the output should not contain "Life is good" diff --git a/features/steps/core.py b/features/steps/core.py index bceab051..f6c54564 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -102,6 +102,11 @@ def check_output_inline(context, text): out = context.stdout_capture.getvalue() assert text in out +@then('the output should not contain "{text}"') +def check_output_not_inline(context, text): + out = context.stdout_capture.getvalue() + assert text not in out + @then('we should see the message "{text}"') def check_message(context, text): out = context.messages.getvalue() From 6dae28f1960c695fc7e9960f7ef74261e23c23a3 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 14 Aug 2013 14:01:13 -0700 Subject: [PATCH 54/91] Version bump --- jrnl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 6c783c61..7fbaf4e5 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.5.0' +__version__ = '1.5.1' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From 2e44a29c33e33a45f8e823646c0999b622fa3251 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 14 Aug 2013 14:06:39 -0700 Subject: [PATCH 55/91] Soft-deprecates `-to` and introduces `-until` --- jrnl/jrnl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 5763a90d..97c01d74 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -39,7 +39,7 @@ def parse_args(args=None): reading = parser.add_argument_group('Reading', 'Specifying either of these parameters will display posts of your journal') reading.add_argument('-from', dest='start_date', metavar="DATE", help='View entries after this date') - reading.add_argument('-to', dest='end_date', metavar="DATE", help='View entries before this date') + reading.add_argument('-until', '-to', dest='end_date', metavar="DATE", help='View entries before this date') reading.add_argument('-and', dest='strict', action="store_true", help='Filter by tags using AND (default: OR)') reading.add_argument('-n', dest='limit', default=None, metavar="N", help='Shows the last n entries matching the filter', nargs="?", type=int) reading.add_argument('-short', dest='short', action="store_true", help='Show only titles or line containing the search tags') From 93813ef0409d78ee007ffdf966ab3946e941f756 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 14 Aug 2013 14:06:53 -0700 Subject: [PATCH 56/91] Updates tests --- features/exporting.feature | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/features/exporting.feature b/features/exporting.feature index 7f58876f..5b3e940f 100644 --- a/features/exporting.feature +++ b/features/exporting.feature @@ -10,13 +10,13 @@ Feature: Expoting a Journal and "tags" in the json output should contain "@journal" and "tags" in the json output should contain "@dan" - Scenario: Exporting using filters should only export parts of the journal - Given we use the config "tags.json" - When we run "jrnl -to 'may 2013' --export json" - # Then we should get no error - Then the output should be parsable as json - 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 "@journal" - and "tags" in the json output should not contain "@dan" + Scenario: Exporting using filters should only export parts of the journal + Given we use the config "tags.json" + When we run "jrnl -until 'may 2013' --export json" + # Then we should get no error + Then the output should be parsable as json + 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 "@journal" + and "tags" in the json output should not contain "@dan" From 2593b23d261eb595b0844868360974bf180b21d8 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 14 Aug 2013 14:07:57 -0700 Subject: [PATCH 57/91] Version bump --- jrnl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 7fbaf4e5..4ee7ee31 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.5.1' +__version__ = '1.5.2' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From 24ff65d6fc56819c360ad4cc00f739cd7a6f0157 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 14 Aug 2013 14:08:05 -0700 Subject: [PATCH 58/91] Readme + changelog update --- CHANGELOG.md | 8 ++++++++ README.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca77eb20..cebd5e32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ Changelog ========= +### 1.5.2 + +* [Improved] Soft-deprecated `-to` for filtering by time and introduces `-until` instead. + +### 1.5.1 + +* [Fixed] Fixed a bug introduced in 1.5.0 that caused the entire journal to be printed after composing an entry + ### 1.5.0 * [Improved] Exporting, encrypting and displaying tags now takes your filter options into account. So you could export everything before May 2012: `jrnl -to 'may 2012' --export json`. Or encrypt all entries tagged with `@work` into a new journal: `jrnl @work --encrypt work_journal.txt`. Or display all tags of posts where Bob is also tagged: `jrnl @bob --tags` diff --git a/README.md b/README.md index 71fe1716..e92ba029 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ _jrnl_ has two modes: __composing__ and __viewing__. will list you the ten latest entries, - jrnl -from "last year" -to march + jrnl -from "last year" -until march everything that happened from the start of last year to the start of last march. If you only want to see the titles of your entries, use From 58ea57f62bb23ac01b8b47dd54ee2e8661e29db2 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 17 Aug 2013 11:54:21 -0700 Subject: [PATCH 59/91] Fixes #87 --- jrnl/Journal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 9a7767e8..3d8eb479 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -315,7 +315,7 @@ class DayOne(Journal): dict_entry = plistlib.readPlist(plist_entry) try: timezone = pytz.timezone(dict_entry['Time Zone']) - except pytz.exceptions.UnknownTimeZoneError: + except (KeyError, pytz.exceptions.UnknownTimeZoneError): timezone = pytz.timezone(util.get_local_timezone()) date = dict_entry['Creation Date'] date = date + timezone.utcoffset(date) From 844690813979d4f3845e61e5445d388799f51812 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 17 Aug 2013 11:59:51 -0700 Subject: [PATCH 60/91] Initial support for DayOne integration testing --- features/data/configs/dayone.json | 14 ++++++ .../044F3747A38546168B572C2E3F217FA2.doentry | 34 ++++++++++++++ .../0BDDD6CDA43C4A9AA2681517CC35AD9D.doentry | 46 +++++++++++++++++++ .../422BC895507944A291E6FC44FC6B8BFC.doentry | 31 +++++++++++++ .../4BB1F46946AD439996C9B59DE7C4DDC1.doentry | 29 ++++++++++++ features/dayone.feature | 17 +++++++ features/environment.py | 6 ++- 7 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 features/data/configs/dayone.json create mode 100644 features/data/journals/dayone.dayone/entries/044F3747A38546168B572C2E3F217FA2.doentry create mode 100644 features/data/journals/dayone.dayone/entries/0BDDD6CDA43C4A9AA2681517CC35AD9D.doentry create mode 100644 features/data/journals/dayone.dayone/entries/422BC895507944A291E6FC44FC6B8BFC.doentry create mode 100644 features/data/journals/dayone.dayone/entries/4BB1F46946AD439996C9B59DE7C4DDC1.doentry create mode 100644 features/dayone.feature diff --git a/features/data/configs/dayone.json b/features/data/configs/dayone.json new file mode 100644 index 00000000..2baf0080 --- /dev/null +++ b/features/data/configs/dayone.json @@ -0,0 +1,14 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": false, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "", + "journals": { + "default": "features/journals/dayone.dayone" + }, + "tagsymbols": "@" +} diff --git a/features/data/journals/dayone.dayone/entries/044F3747A38546168B572C2E3F217FA2.doentry b/features/data/journals/dayone.dayone/entries/044F3747A38546168B572C2E3F217FA2.doentry new file mode 100644 index 00000000..2bb8e3c3 --- /dev/null +++ b/features/data/journals/dayone.dayone/entries/044F3747A38546168B572C2E3F217FA2.doentry @@ -0,0 +1,34 @@ + + + + + Creation Date + 2013-05-17T18:39:20Z + Creator + + Device Agent + Macintosh/MacBookAir5,2 + Generation Date + 2013-08-17T18:39:20Z + Host Name + Egeria + OS Agent + Mac OS X/10.8.4 + Software Agent + Day One (Mac)/1.8 + + Entry Text + This entry has tags! + Starred + + Tags + + work + play + + Time Zone + America/Los_Angeles + UUID + 044F3747A38546168B572C2E3F217FA2 + + diff --git a/features/data/journals/dayone.dayone/entries/0BDDD6CDA43C4A9AA2681517CC35AD9D.doentry b/features/data/journals/dayone.dayone/entries/0BDDD6CDA43C4A9AA2681517CC35AD9D.doentry new file mode 100644 index 00000000..927de884 --- /dev/null +++ b/features/data/journals/dayone.dayone/entries/0BDDD6CDA43C4A9AA2681517CC35AD9D.doentry @@ -0,0 +1,46 @@ + + + + + Creation Date + 2013-06-17T18:38:29Z + Creator + + Device Agent + Macintosh/MacBookAir5,2 + Generation Date + 2013-08-17T18:38:29Z + Host Name + Egeria + OS Agent + Mac OS X/10.8.4 + Software Agent + Day One (Mac)/1.8 + + Entry Text + This entry has a location. + Location + + Administrative Area + California + Country + Germany + Latitude + 52.4979764 + Locality + Berlin + Longitude + 13.2404758 + Place Name + Abandoned Spy Tower + + Starred + + Tags + + Time Zone + Europe/Berlin + UUID + 0BDDD6CDA43C4A9AA2681517CC35AD9D + + diff --git a/features/data/journals/dayone.dayone/entries/422BC895507944A291E6FC44FC6B8BFC.doentry b/features/data/journals/dayone.dayone/entries/422BC895507944A291E6FC44FC6B8BFC.doentry new file mode 100644 index 00000000..16260763 --- /dev/null +++ b/features/data/journals/dayone.dayone/entries/422BC895507944A291E6FC44FC6B8BFC.doentry @@ -0,0 +1,31 @@ + + + + + Creation Date + 2013-07-17T18:38:08Z + Creator + + Device Agent + Macintosh/MacBookAir5,2 + Generation Date + 2013-08-17T18:38:08Z + Host Name + Egeria + OS Agent + Mac OS X/10.8.4 + Software Agent + Day One (Mac)/1.8 + + Entry Text + This entry is starred! + Starred + + Tags + + Time Zone + America/Los_Angeles + UUID + 422BC895507944A291E6FC44FC6B8BFC + + diff --git a/features/data/journals/dayone.dayone/entries/4BB1F46946AD439996C9B59DE7C4DDC1.doentry b/features/data/journals/dayone.dayone/entries/4BB1F46946AD439996C9B59DE7C4DDC1.doentry new file mode 100644 index 00000000..977026d7 --- /dev/null +++ b/features/data/journals/dayone.dayone/entries/4BB1F46946AD439996C9B59DE7C4DDC1.doentry @@ -0,0 +1,29 @@ + + + + + Creation Date + 2013-08-17T18:37:50Z + Creator + + Device Agent + Macintosh/MacBookAir5,2 + Generation Date + 2013-08-17T18:37:50Z + Host Name + Egeria + OS Agent + Mac OS X/10.8.4 + Software Agent + Day One (Mac)/1.8 + + Entry Text + This is a DayOne entry. + Starred + + Tags + + UUID + 4BB1F46946AD439996C9B59DE7C4DDC1 + + diff --git a/features/dayone.feature b/features/dayone.feature new file mode 100644 index 00000000..898699e1 --- /dev/null +++ b/features/dayone.feature @@ -0,0 +1,17 @@ +Feature: DayOne Ingetration + + Scenario: Loading a DayOne Journal + Given we use the config "dayone.json" + When we run "jrnl -until now" + Then we should get no error + and the output should be + """ + 2013-05-17 11:39 This entry has tags! + + 2013-06-17 20:38 This entry has a location. + + 2013-07-17 11:38 This entry is starred! + + 2013-08-17 11:37 This is a DayOne entry. + """ + diff --git a/features/environment.py b/features/environment.py index 89125fca..c5b96721 100644 --- a/features/environment.py +++ b/features/environment.py @@ -18,7 +18,11 @@ def before_scenario(context, scenario): if not os.path.exists(working_dir): os.mkdir(working_dir) for filename in os.listdir(original): - shutil.copy2(os.path.join(original, filename), working_dir) + source = os.path.join(original, filename) + if os.path.isdir(source): + shutil.copytree(source, os.path.join(working_dir, filename)) + else: + shutil.copy2(source, working_dir) def after_scenario(context, scenario): """After each scenario, restore all test data and remove working_dirs.""" From f3e1f94f551aa823a4dedaed7a23724b3878a1eb Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 17 Aug 2013 12:06:52 -0700 Subject: [PATCH 61/91] Support for DayOne tagging Closes #83 --- jrnl/Journal.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 3d8eb479..61d6c618 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -322,6 +322,7 @@ class DayOne(Journal): entry = self.new_entry(raw=dict_entry['Entry Text'], date=date, sort=False) entry.starred = dict_entry["Starred"] entry.uuid = dict_entry["UUID"] + entry.tags = dict_entry.get("Tags", []) # We're using new_entry to create the Entry object, which adds the entry # to self.entries already. However, in the original Journal.__init__, this # method is expected to return a list of newly created entries, which is why @@ -343,6 +344,9 @@ class DayOne(Journal): 'Starred': entry.starred if hasattr(entry, 'starred') else False, 'Entry Text': entry.title+"\n"+entry.body, 'Time Zone': util.get_local_timezone(), - 'UUID': new_uuid + 'UUID': new_uuid, + 'Tags': [tag.strip(self.config['tagsymbols']) for tag in entry.tags] } + # print entry_plist + plistlib.writePlist(entry_plist, filename) From 59de6351bc320a1d8169a53950e97a6302bd5e2e Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 17 Aug 2013 12:06:59 -0700 Subject: [PATCH 62/91] Tests for DayOne Tagging --- features/dayone.feature | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/features/dayone.feature b/features/dayone.feature index 898699e1..27f795f5 100644 --- a/features/dayone.feature +++ b/features/dayone.feature @@ -15,3 +15,30 @@ Feature: DayOne Ingetration 2013-08-17 11:37 This is a DayOne entry. """ + Scenario: Writing into Dayone + Given we use the config "dayone.json" + When we run "jrnl 01 may 1979: Being born hurts." + and we run "jrnl -until 1980" + Then the output should be + """ + 1979-05-01 09:00 Being born hurts. + """ + + Scenario: Loading tags from a DayOne Journal + Given we use the config "dayone.json" + When we run "jrnl --tags" + Then the output should be + """ + work : 1 + play : 1 + """ + + Scenario: Saving tags from a DayOne Journal + Given we use the config "dayone.json" + When we run "jrnl A hard day at @work" + and we run "jrnl --tags" + Then the output should be + """ + work : 2 + play : 1 + """ From 19d342d6f2315e2d4c66e65c1cd6fff015557491 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 17 Aug 2013 12:09:57 -0700 Subject: [PATCH 63/91] Version bump and doc fixes --- CHANGELOG.md | 12 ++++++++++-- README.md | 8 ++++---- jrnl/__init__.py | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cebd5e32..e5ec166f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,19 @@ Changelog ========= -### 1.5.2 +#### 1.5.4 + +* [New] DayOne journals can now handle tags + +#### 1.5.3 + +* [Fixed] DayOne integration with older DayOne Journals + +#### 1.5.2 * [Improved] Soft-deprecated `-to` for filtering by time and introduces `-until` instead. -### 1.5.1 +#### 1.5.1 * [Fixed] Fixed a bug introduced in 1.5.0 that caused the entire journal to be printed after composing an entry diff --git a/README.md b/README.md index e92ba029..52d65b52 100644 --- a/README.md +++ b/README.md @@ -204,11 +204,11 @@ The configuration file is a simple JSON file with the following options. ### DayOne Integration -Using your DayOne journal instead of a flat text file is dead simple - instead of pointing to a text file, set the `"journal"` key in your `.jrnl_conf` to point to your DayOne journal. This is a folder ending with `.dayone`, and it's located at +Using your DayOne journal instead of a flat text file is dead simple - instead of pointing to a text file, change your `.jrnl_conf` to point to your DayOne journal. This is a folder ending with `.dayone`, and it's located at - * `~/Library/Application Support/Day One/` by default - * `~/Dropbox/Apps/Day One/` if you're syncing with Dropbox and - * `~/Library/Mobile Documents/5U8NS4GX82~com~dayoneapp~dayone/Documents/` if you're syncing with iCloud. +* `~/Library/Application Support/Day One/` by default +* `~/Dropbox/Apps/Day One/` if you're syncing with Dropbox and +* `~/Library/Mobile Documents/5U8NS4GX82~com~dayoneapp~dayone/Documents/` if you're syncing with iCloud. Instead of all entries being in a single file, each entry will live in a separate `plist` file. You can also star entries when you write them: diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 4ee7ee31..01168bec 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.5.2' +__version__ = '1.5.4' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From 575026b51dc1058a5c3d8cfef69a96d4cfdb23e8 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 17 Aug 2013 12:18:48 -0700 Subject: [PATCH 64/91] Some testing goodies --- features/steps/core.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/features/steps/core.py b/features/steps/core.py index f6c54564..1b476a2a 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -66,15 +66,15 @@ def no_error(context): @then('the output should be parsable as json') def check_output_json(context): out = context.stdout_capture.getvalue() - assert json.loads(out) + 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 - assert len(out_json[field]) == number + 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): @@ -95,7 +95,7 @@ def check_output(context): text = context.text.strip().splitlines() out = context.stdout_capture.getvalue().strip().splitlines() for line_text, line_out in zip(text, out): - assert line_text.strip() == line_out.strip() + assert line_text.strip() == line_out.strip(), [line_text.strip(), line_out.strip()] @then('the output should contain "{text}"') def check_output_inline(context, text): From 81e8e26b0a47e1b3803152029810a70cb246bf3f Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 17 Aug 2013 13:19:54 -0700 Subject: [PATCH 65/91] Tests for parsing DayOne entries without Timezone --- .../4BB1F46946AD439996C9B59DE7C4DDC1.doentry | 4 ++-- features/dayone.feature | 10 +++++++--- features/steps/core.py | 15 +++++++++++++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/features/data/journals/dayone.dayone/entries/4BB1F46946AD439996C9B59DE7C4DDC1.doentry b/features/data/journals/dayone.dayone/entries/4BB1F46946AD439996C9B59DE7C4DDC1.doentry index 977026d7..9ebaf538 100644 --- a/features/data/journals/dayone.dayone/entries/4BB1F46946AD439996C9B59DE7C4DDC1.doentry +++ b/features/data/journals/dayone.dayone/entries/4BB1F46946AD439996C9B59DE7C4DDC1.doentry @@ -3,7 +3,7 @@ Creation Date - 2013-08-17T18:37:50Z + 2013-01-17T18:37:50Z Creator Device Agent @@ -18,7 +18,7 @@ Day One (Mac)/1.8 Entry Text - This is a DayOne entry. + This is a DayOne entry without Timezone. Starred Tags diff --git a/features/dayone.feature b/features/dayone.feature index 27f795f5..c8e987e1 100644 --- a/features/dayone.feature +++ b/features/dayone.feature @@ -2,7 +2,7 @@ Feature: DayOne Ingetration Scenario: Loading a DayOne Journal Given we use the config "dayone.json" - When we run "jrnl -until now" + When we run "jrnl -from 'feb 2013'" Then we should get no error and the output should be """ @@ -11,10 +11,14 @@ Feature: DayOne Ingetration 2013-06-17 20:38 This entry has a location. 2013-07-17 11:38 This entry is starred! - - 2013-08-17 11:37 This is a DayOne entry. """ + Scenario: Entries without timezone information will be intepreted in the current timezone + Given we use the config "dayone.json" + When we run "jrnl -until 'feb 2013'" + Then we should get no error + and the output should contain "2013-01-17T18:37Z" in the local time + Scenario: Writing into Dayone Given we use the config "dayone.json" When we run "jrnl 01 may 1979: Being born hurts." diff --git a/features/steps/core.py b/features/steps/core.py index 1b476a2a..66e6a07f 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -1,8 +1,10 @@ from behave import * -from jrnl import jrnl, Journal +from jrnl import jrnl, Journal, util +from dateutil import parser as date_parser import os import sys import json +import pytz try: from io import StringIO except ImportError: @@ -73,7 +75,7 @@ def check_output_json(context): 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 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}"') @@ -97,6 +99,15 @@ def check_output(context): for line_text, line_out in zip(text, out): assert line_text.strip() == line_out.strip(), [line_text.strip(), line_out.strip()] +@then('the output should contain "{text}" in the local time') +def check_output_time_inline(context, text): + out = context.stdout_capture.getvalue() + local_tz = pytz.timezone(util.get_local_timezone()) + utc_time = date_parser.parse(text) + date = utc_time + local_tz._utcoffset + local_date = date.strftime("%Y-%m-%d %H:%M") + assert local_date in out, local_date + @then('the output should contain "{text}"') def check_output_inline(context, text): out = context.stdout_capture.getvalue() From e796f18d6ec341a77ce0798d5961a46f439abd3c Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 17 Aug 2013 13:24:51 -0700 Subject: [PATCH 66/91] Updates travis build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 15cf9c92..87e5f3b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ python: - "3.3" install: - "pip install -q -r requirements.txt --use-mirrors" - - "pip install -q behave" + - "pip install -q behave dateutil" # command to run tests script: - python --version From d36711b36b878a5170c52247c8ed8a6d575ee630 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sat, 17 Aug 2013 18:14:06 -0700 Subject: [PATCH 67/91] dateutil -> python-dateutil --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 87e5f3b7..58acbc98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ python: - "3.3" install: - "pip install -q -r requirements.txt --use-mirrors" - - "pip install -q behave dateutil" + - "pip install -q behave python-dateutil" # command to run tests script: - python --version From 79b61dfbab3bcc038dcf2fb1252aa475a8a10e05 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 28 Aug 2013 12:18:39 -0700 Subject: [PATCH 68/91] Detectsa DayOne journals that are stored in the Mobile Documents folder --- jrnl/jrnl.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 97c01d74..66ac4feb 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -150,13 +150,16 @@ def cli(manual_args=None): mode_compose, mode_export = guess_mode(args, config) # open journal file or folder - if os.path.isdir(config['journal']) and ( config['journal'].endswith(".dayone") or \ - config['journal'].endswith(".dayone/")): - journal = Journal.DayOne(**config) + if os.path.isdir(config['journal']): + if config['journal'].strip("/").endswith(".dayone") or \ + "entries" in os.listdir(config['journal']): + journal = Journal.DayOne(**config) + else: + util.prompt("[Error: {} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])) + sys.exit(-1) else: journal = Journal.Journal(**config) - if mode_compose and not args.text: if config['editor']: raw = get_text_from_editor(config) From a7afed7c8bdba83f61d4a5e4e52c539cf689085e Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 28 Aug 2013 12:18:58 -0700 Subject: [PATCH 69/91] Tests for error messages on empty directories --- features/data/configs/empty_folder.json | 14 ++++++++++++++ features/environment.py | 11 ++++++++++- features/regression.feature | 7 +++++++ features/steps/core.py | 18 +++++++++++++++--- 4 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 features/data/configs/empty_folder.json diff --git a/features/data/configs/empty_folder.json b/features/data/configs/empty_folder.json new file mode 100644 index 00000000..d270db03 --- /dev/null +++ b/features/data/configs/empty_folder.json @@ -0,0 +1,14 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": false, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "", + "journals": { + "default": "features/journals/empty_folder" + }, + "tagsymbols": "@" +} diff --git a/features/environment.py b/features/environment.py index c5b96721..bbf59226 100644 --- a/features/environment.py +++ b/features/environment.py @@ -12,6 +12,14 @@ def before_scenario(context, scenario): context.messages = StringIO() jrnl.util.STDERR = context.messages jrnl.util.TEST = True + + # Clean up in case something went wrong + for folder in ("configs", "journals"): + working_dir = os.path.join("features", folder) + if os.path.exists(working_dir): + shutil.rmtree(working_dir) + + for folder in ("configs", "journals"): original = os.path.join("features", "data", folder) working_dir = os.path.join("features", folder) @@ -30,4 +38,5 @@ def after_scenario(context, scenario): context.messages = None for folder in ("configs", "journals"): working_dir = os.path.join("features", folder) - shutil.rmtree(working_dir) + if os.path.exists(working_dir): + shutil.rmtree(working_dir) diff --git a/features/regression.feature b/features/regression.feature index c4cf41e7..da0ef277 100644 --- a/features/regression.feature +++ b/features/regression.feature @@ -1,8 +1,15 @@ Feature: Zapped bugs should stay dead. Scenario: Writing an entry does not print the entire journal + # https://github.com/maebert/jrnl/issues/87 Given we use the config "basic.json" When we run "jrnl 23 july 2013: A cold and stormy day. I ate crisps on the sofa." Then we should see the message "Entry added" 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.json" + 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" diff --git a/features/steps/core.py b/features/steps/core.py index 66e6a07f..d805e12d 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -53,16 +53,28 @@ def run_with_input(context, command, inputs=None): args = _parse_args(command) buffer = StringIO(text.strip()) jrnl.util.STDIN = buffer - jrnl.cli(args) + try: + jrnl.cli(args or None) + context.exit_status = 0 + except SystemExit as e: + context.exit_status = e.code @when('we run "{command}"') def run(context, command): args = _parse_args(command) - jrnl.cli(args or None) + try: + jrnl.cli(args or None) + context.exit_status = 0 + except SystemExit as e: + context.exit_status = e.code +@then('we should get an error') +def has_error(context): + assert context.exit_status is not 0 @then('we should get no error') def no_error(context): + assert context.exit_status is 0 assert context.failed is False @then('the output should be parsable as json') @@ -121,7 +133,7 @@ def check_output_not_inline(context, text): @then('we should see the message "{text}"') def check_message(context, text): out = context.messages.getvalue() - assert text in out + assert text in out, [text, out] @then('the journal should contain "{text}"') @then('journal "{journal_name}" should contain "{text}"') From 8f85b97942ef550409a1cc9082c6eeaf733b8995 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 28 Aug 2013 12:19:03 -0700 Subject: [PATCH 70/91] Version bump --- CHANGELOG.md | 4 ++++ jrnl/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5ec166f..20fac307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +#### 1.5.5 + +* [Fixed] Detects DayOne journals stored in `~/Library/Mobile Data` as well. + #### 1.5.4 * [New] DayOne journals can now handle tags diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 01168bec..28fe63cd 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.5.4' +__version__ = '1.5.5' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From e37395ffd9b4c44a8bc4f45af3a08d829069e49b Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 28 Aug 2013 12:29:34 -0700 Subject: [PATCH 71/91] Failures return exit code 1 --- jrnl/jrnl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 66ac4feb..7f30ad49 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -125,14 +125,14 @@ def cli(manual_args=None): except ValueError as e: util.prompt("[There seems to be something wrong with your jrnl config at {}: {}]".format(CONFIG_PATH, e.message)) util.prompt("[Entry was NOT added to your journal]") - sys.exit(-1) + sys.exit(1) install.update_config(config, config_path=CONFIG_PATH) original_config = config.copy() # check if the configuration is supported by available modules if config['encrypt'] and not PYCRYPTO: util.prompt("According to your jrnl_conf, your journal is encrypted, however PyCrypto was not found. To open your journal, install the PyCrypto package from http://www.pycrypto.org.") - sys.exit(-1) + sys.exit(1) args = parse_args(manual_args) @@ -156,7 +156,7 @@ def cli(manual_args=None): journal = Journal.DayOne(**config) else: util.prompt("[Error: {} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])) - sys.exit(-1) + sys.exit(1) else: journal = Journal.Journal(**config) From 3d4993733544360e380a869dc4516cd8fed01941 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 28 Aug 2013 12:36:00 -0700 Subject: [PATCH 72/91] Better checking for failed runs --- .gitignore | 1 + features/steps/core.py | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 03ffc7d3..deb6c96d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ lib64 # Installer logs pip-log.txt .DS_Store +.travis-solo diff --git a/features/steps/core.py b/features/steps/core.py index d805e12d..da39387f 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -70,12 +70,11 @@ def run(context, command): @then('we should get an error') def has_error(context): - assert context.exit_status is not 0 + assert context.exit_status != 0, context.exit_status @then('we should get no error') def no_error(context): - assert context.exit_status is 0 - assert context.failed is False + assert context.exit_status is 0, context.exit_status @then('the output should be parsable as json') def check_output_json(context): From d4aa08e18a082029544b98dbea5697798ce0fa91 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 28 Aug 2013 13:37:18 -0700 Subject: [PATCH 73/91] Adds empty folder for testing --- features/data/journals/empty_folder/empty.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 features/data/journals/empty_folder/empty.txt diff --git a/features/data/journals/empty_folder/empty.txt b/features/data/journals/empty_folder/empty.txt new file mode 100644 index 00000000..175b82b5 --- /dev/null +++ b/features/data/journals/empty_folder/empty.txt @@ -0,0 +1 @@ +Nothing to see here From 2235a2f974ecf089ba32636ea097cb717bd9d59c Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Wed, 28 Aug 2013 13:48:18 -0700 Subject: [PATCH 74/91] Python 2.6 compatibility --- jrnl/Entry.py | 2 +- jrnl/Journal.py | 4 ++-- jrnl/exporters.py | 8 ++++---- jrnl/jrnl.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/jrnl/Entry.py b/jrnl/Entry.py index 5fbdeb15..32c76b12 100644 --- a/jrnl/Entry.py +++ b/jrnl/Entry.py @@ -58,7 +58,7 @@ class Entry: ) def __repr__(self): - return "".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M")) + return "".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M")) def to_dict(self): return { diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 61d6c618..11b31952 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -181,7 +181,7 @@ class Journal(object): return self.__unicode__() def __repr__(self): - return "".format(len(self.entries)) + return "".format(len(self.entries)) def write(self, filename=None): """Dumps the journal into the config file, overwriting it""" @@ -234,7 +234,7 @@ class Journal(object): for m in matches: date = e.date.strftime(self.config['timeformat']) excerpt = e.body[m.start():min(len(e.body), m.end()+60)] - res.append('{} {} ..'.format(date, excerpt)) + res.append('{0} {1} ..'.format(date, excerpt)) e.body = "\n".join(res) else: for e in self.entries: diff --git a/jrnl/exporters.py b/jrnl/exporters.py index 42126802..7d2eaa93 100644 --- a/jrnl/exporters.py +++ b/jrnl/exporters.py @@ -87,16 +87,16 @@ def export(journal, format, output=None): try: with open(output, 'w') as f: f.write(content) - return "[Journal exported to {}]".format(output) + return "[Journal exported to {0}]".format(output) except IOError as e: - return "[ERROR: {} {}]".format(e.filename, e.strerror) + return "[ERROR: {0} {1}]".format(e.filename, e.strerror) else: return content def write_files(journal, path, format): """Turns your journal into separate files for each entry. Format should be either json, md or txt.""" - make_filename = lambda entry: e.date.strftime("%C-%m-%d_{}.{}".format(slugify(u(e.title)), format)) + make_filename = lambda entry: e.date.strftime("%C-%m-%d_{0}.{1}".format(slugify(u(e.title)), format)) for e in journal.entries: full_path = os.path.join(path, make_filename(e)) if format == 'json': @@ -107,4 +107,4 @@ def write_files(journal, path, format): content = u(e) with open(full_path, 'w') as f: f.write(content) - return "[Journal exported individual files in {}]".format(path) + return "[Journal exported individual files in {0}]".format(path) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 7f30ad49..639e33d0 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -123,7 +123,7 @@ def cli(manual_args=None): try: config = json.load(f) except ValueError as e: - util.prompt("[There seems to be something wrong with your jrnl config at {}: {}]".format(CONFIG_PATH, e.message)) + util.prompt("[There seems to be something wrong with your jrnl config at {0}: {1}]".format(CONFIG_PATH, e.message)) util.prompt("[Entry was NOT added to your journal]") sys.exit(1) install.update_config(config, config_path=CONFIG_PATH) @@ -155,7 +155,7 @@ def cli(manual_args=None): "entries" in os.listdir(config['journal']): journal = Journal.DayOne(**config) else: - util.prompt("[Error: {} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])) + util.prompt("[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])) sys.exit(1) else: journal = Journal.Journal(**config) From 1246fb2bbb0fabc6d1ec5e99efdee5f919bb5806 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 16 Sep 2013 10:45:57 -0700 Subject: [PATCH 75/91] Fixes #93 --- jrnl/util.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/jrnl/util.py b/jrnl/util.py index 28499933..4fc2575b 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -4,6 +4,7 @@ import sys import os from tzlocal import get_localzone import getpass as gp +import pytz PY3 = sys.version_info[0] == 3 PY2 = sys.version_info[0] == 2 @@ -41,6 +42,13 @@ def get_local_timezone(): global __cached_tz if not __cached_tz and "darwin" in sys.platform: __cached_tz = os.popen("systemsetup -gettimezone").read().replace("Time Zone: ", "").strip() - elif not __cached_tz: + if not __cached_tz or __cached_tz not in pytz.all_timezones_set: + link = os.readlink("/etc/localtime") + # This is something like /usr/share/zoneinfo/America/Los_Angeles. + # Find second / from right and take the substring + __cached_tz = link[link.rfind('/', 0, link.rfind('/'))+1:] + if not __cached_tz or __cached_tz not in pytz.all_timezones_set: __cached_tz = str(get_localzone()) + if not __cached_tz or __cached_tz not in pytz.all_timezones_set: + __cached_tz = "UTC" return __cached_tz From 7315aa8c06a0871d9783b06b87414d6a261a5046 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 16 Sep 2013 10:46:02 -0700 Subject: [PATCH 76/91] Version bump --- CHANGELOG.md | 4 ++++ jrnl/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20fac307..4aa0be7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ Changelog #### 1.5.5 +* [Fixed] Fixed a bug where on OS X, the timezone could only be accessed on administrator accounts. + +#### 1.5.5 + * [Fixed] Detects DayOne journals stored in `~/Library/Mobile Data` as well. #### 1.5.4 diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 28fe63cd..abed3311 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.5.5' +__version__ = '1.5.6' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From 08d260f3b2a90704c871a3757386dea85708bf55 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 16 Sep 2013 10:57:37 -0700 Subject: [PATCH 77/91] Treat non-OS X separately --- jrnl/util.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/jrnl/util.py b/jrnl/util.py index 4fc2575b..4f6031d5 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -42,12 +42,12 @@ def get_local_timezone(): global __cached_tz if not __cached_tz and "darwin" in sys.platform: __cached_tz = os.popen("systemsetup -gettimezone").read().replace("Time Zone: ", "").strip() - if not __cached_tz or __cached_tz not in pytz.all_timezones_set: - link = os.readlink("/etc/localtime") - # This is something like /usr/share/zoneinfo/America/Los_Angeles. - # Find second / from right and take the substring - __cached_tz = link[link.rfind('/', 0, link.rfind('/'))+1:] - if not __cached_tz or __cached_tz not in pytz.all_timezones_set: + if not __cached_tz or __cached_tz not in pytz.all_timezones_set: + link = os.readlink("/etc/localtime") + # This is something like /usr/share/zoneinfo/America/Los_Angeles. + # Find second / from right and take the substring + __cached_tz = link[link.rfind('/', 0, link.rfind('/'))+1:] + elif not __cached_tz: __cached_tz = str(get_localzone()) if not __cached_tz or __cached_tz not in pytz.all_timezones_set: __cached_tz = "UTC" From 8f5082a91986ed2798aabc1670f0ee5031524f61 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 23 Sep 2013 17:24:23 -0700 Subject: [PATCH 78/91] Fixes #91 --- jrnl/jrnl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 639e33d0..acd5eff9 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -146,6 +146,7 @@ def cli(manual_args=None): config.update(journal_conf) else: # But also just give them a string to point to the journal file config['journal'] = journal_conf + config['journal'] = os.path.expanduser(config['journal']) touch_journal(config['journal']) mode_compose, mode_export = guess_mode(args, config) From f5826fb56bdc0d320c763930422ed0ac78d833de Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 23 Sep 2013 17:24:29 -0700 Subject: [PATCH 79/91] Version bump --- CHANGELOG.md | 6 +++++- jrnl/__init__.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aa0be7c..0e231dd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ Changelog ========= -#### 1.5.5 +#### 1.5.7 + +* [Improved] The `~` in journal config paths will now expand properly to e.g. `/Users/maebert` + +#### 1.5.6 * [Fixed] Fixed a bug where on OS X, the timezone could only be accessed on administrator accounts. diff --git a/jrnl/__init__.py b/jrnl/__init__.py index abed3311..42dce17e 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.5.6' +__version__ = '1.5.7' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From d588b31a7114a7f82acf123f20bcea23fcffbf58 Mon Sep 17 00:00:00 2001 From: Bitdeli Chef Date: Tue, 8 Oct 2013 17:57:54 +0000 Subject: [PATCH 80/91] Add a Bitdeli badge to README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 52d65b52..90f896d5 100644 --- a/README.md +++ b/README.md @@ -263,3 +263,7 @@ Known Issues - The Windows shell prior to Windows 7 has issues with unicode encoding. If you want to use non-ascii characters, change the codepage with `chcp 1252` before using `jrnl` (Thanks to Yves Pouplard for solving this!) - _jrnl_ relies on the `PyCrypto` package to encrypt journals, which has some known problems with installing on Windows and within virtual environments. If you have trouble installing __jrnl__, [install PyCyrypto manually](https://www.dlitz.net/software/pycrypto/) first. + + +[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/maebert/jrnl/trend.png)](https://bitdeli.com/free "Bitdeli Badge") + From be2c511ea4c6323decc3542431c1c2dd9e1556d5 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 17 Oct 2013 14:55:59 -0700 Subject: [PATCH 81/91] Saves password to keyring Closes #96 and deprecates password field in config --- CHANGELOG.md | 4 ++++ jrnl/Journal.py | 41 +++++++++++++---------------------------- jrnl/__init__.py | 2 +- jrnl/install.py | 7 ++++--- jrnl/jrnl.py | 22 +++++++++++----------- jrnl/util.py | 38 +++++++++++++++++++++++++++++++++++++- requirements.txt | 1 + setup.py | 3 ++- 8 files changed, 73 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e231dd9..e94bf47c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ========= +#### 1.6.0 + +* [Improved] Passwords are now saved in the key-chain. The `password` field in `.jrnl_config` is soft-deprecated. + #### 1.5.7 * [Improved] The `~` in journal config paths will now expand properly to e.g. `/Users/maebert` diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 11b31952..116b6c9a 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -12,18 +12,13 @@ except ImportError: import parsedatetime.parsedatetime as pdt import re from datetime import datetime import time -try: import simplejson as json -except ImportError: import json import sys -import glob try: from Crypto.Cipher import AES from Crypto import Random crypto_installed = True except ImportError: crypto_installed = False -if "win32" in sys.platform: import pyreadline as readline -else: import readline import hashlib try: import colorama @@ -33,14 +28,13 @@ except ImportError: import plistlib import pytz import uuid - +from functools import partial class Journal(object): - def __init__(self, **kwargs): + def __init__(self, name='default', **kwargs): self.config = { 'journal': "journal.txt", 'encrypt': False, - 'password': "", 'default_hour': 9, 'default_minute': 0, 'timeformat': "%Y-%m-%d %H:%M", @@ -54,7 +48,8 @@ class Journal(object): consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday self.dateparse = pdt.Calendar(consts) self.key = None # used to decrypt and encrypt the journal - self.search_tags = None # Store tags we're highlighting + self.search_tags = None # Store tags we're highlighting + self.name = name journal_txt = self.open() self.entries = self.parse(journal_txt) @@ -77,7 +72,7 @@ class Journal(object): plain = crypto.decrypt(cipher[16:]) except ValueError: util.prompt("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?") - sys.exit(-1) + sys.exit(1) padding = " ".encode("utf-8") if not plain.endswith(padding): # Journals are always padded return None @@ -95,33 +90,23 @@ class Journal(object): plain += b" " * (AES.block_size - len(plain) % AES.block_size) return iv + crypto.encrypt(plain) - def make_key(self, prompt="Password: "): + def make_key(self, password): """Creates an encryption key from the default password or prompts for a new password.""" - password = self.config['password'] or util.getpass(prompt) self.key = hashlib.sha256(password.encode("utf-8")).digest() def open(self, filename=None): """Opens the journal file defined in the config and parses it into a list of Entries. Entries have the form (date, title, body).""" filename = filename or self.config['journal'] - journal = None + + def validate_password(journal, password): + self.make_key(password) + return self._decrypt(journal) + if self.config['encrypt']: with open(filename, "rb") as f: - journal = f.read() - decrypted = None - attempts = 0 - while decrypted is None: - self.make_key() - decrypted = self._decrypt(journal) - if decrypted is None: - attempts += 1 - self.config['password'] = None # This password doesn't work. - if attempts < 3: - util.prompt("Wrong password, try again.") - else: - util.prompt("Extremely wrong password.") - sys.exit(-1) - journal = decrypted + journal_encrypted = f.read() + journal = util.get_password(keychain=self.name, validator=partial(validate_password, journal_encrypted)) else: with codecs.open(filename, "r", "utf-8") as f: journal = f.read() diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 42dce17e..6124d668 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.5.7' +__version__ = '1.6.0' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' diff --git a/jrnl/install.py b/jrnl/install.py index 2037e8aa..d09e5150 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -73,18 +73,19 @@ def install_jrnl(config_path='~/.jrnl_config'): password = getpass.getpass("Enter password for journal (leave blank for no encryption): ") if password: default_config['encrypt'] = True + if util.yesno("Do you want to store the password in your keychain?", default=True): + util.set_keychain("default", password) print("Journal will be encrypted.") - print("If you want to, you can store your password in .jrnl_config and will never be bothered about it again.") else: password = None - print("PyCrypto not found. To encrypt your journal, install the PyCrypto package from http://www.pycrypto.org and run 'jrnl --encrypt'. For now, your journal will be stored in plain text.") + print("PyCrypto not found. To encrypt your journal, install the PyCrypto package from http://www.pycrypto.org or with 'pip install pycrypto' and run 'jrnl --encrypt'. For now, your journal will be stored in plain text.") # Use highlighting: if not module_exists("colorama"): print("colorama not found. To turn on highlighting, install colorama and set highlight to true in your .jrnl_conf.") default_config['highlight'] = False - open(default_config['journals']['default'], 'a').close() # Touch to make sure it's there + open(default_config['journals']['default'], 'a').close() # Touch to make sure it's there # Write config to ~/.jrnl_conf with open(config_path, 'w') as f: diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index acd5eff9..eec9b227 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -86,10 +86,12 @@ def get_text_from_editor(config): def encrypt(journal, filename=None): """ Encrypt into new file. If filename is not set, we encrypt the journal file itself. """ - journal.config['password'] = "" - journal.make_key(prompt="Enter new password:") + password = util.getpass("Enter new password: ") + journal.make_key(password) journal.config['encrypt'] = True journal.write(filename) + if util.yesno("Do you want to store the password in your keychain?", default=True): + util.set_keychain(journal.name, password) util.prompt("Journal encrypted to {0}.".format(filename or journal.config['journal'])) def decrypt(journal, filename=None): @@ -109,12 +111,11 @@ def update_config(config, new_config, scope): """Updates a config dict with new values - either global if scope is None of config['journals'][scope] is just a string pointing to a journal file, or within the scope""" - if scope and type(config['journals'][scope]) is dict: # Update to journal specific + if scope and type(config['journals'][scope]) is dict: # Update to journal specific config['journals'][scope].update(new_config) else: config.update(new_config) - def cli(manual_args=None): if not os.path.exists(CONFIG_PATH): config = install.install_jrnl(CONFIG_PATH) @@ -142,9 +143,9 @@ def cli(manual_args=None): if journal_name is not 'default': args.text = args.text[1:] journal_conf = config['journals'].get(journal_name) - if type(journal_conf) is dict: # We can override the default config on a by-journal basis + if type(journal_conf) is dict: # We can override the default config on a by-journal basis config.update(journal_conf) - else: # But also just give them a string to point to the journal file + else: # But also just give them a string to point to the journal file config['journal'] = journal_conf config['journal'] = os.path.expanduser(config['journal']) touch_journal(config['journal']) @@ -159,7 +160,7 @@ def cli(manual_args=None): util.prompt("[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])) sys.exit(1) else: - journal = Journal.Journal(**config) + journal = Journal.Journal(journal_name, **config) if mode_compose and not args.text: if config['editor']: @@ -205,22 +206,21 @@ def cli(manual_args=None): encrypt(journal, filename=args.encrypt) # Not encrypting to a separate file: update config! if not args.encrypt: - update_config(original_config, {"encrypt": True, "password": ""}, journal_name) + update_config(original_config, {"encrypt": True}, journal_name) install.save_config(original_config, config_path=CONFIG_PATH) elif args.decrypt is not False: decrypt(journal, filename=args.decrypt) # Not decrypting to a separate file: update config! if not args.decrypt: - update_config(original_config, {"encrypt": False, "password": ""}, journal_name) + update_config(original_config, {"encrypt": False}, journal_name) install.save_config(original_config, config_path=CONFIG_PATH) elif args.delete_last: last_entry = journal.entries.pop() util.prompt("[Deleted Entry:]") - print(last_entry) + print(last_entry.pprint()) journal.write() if __name__ == "__main__": cli() - diff --git a/jrnl/util.py b/jrnl/util.py index 4f6031d5..e0667cab 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -4,6 +4,7 @@ import sys import os from tzlocal import get_localzone import getpass as gp +import keyring import pytz PY3 = sys.version_info[0] == 3 @@ -14,12 +15,42 @@ STDOUT = sys.stdout TEST = False __cached_tz = None -def getpass(prompt): +def getpass(prompt="Password: "): if not TEST: return gp.getpass(prompt) else: return py23_input(prompt) +def get_password(validator, keychain=None, max_attempts=3): + pwd_from_keychain = keychain and get_keychain(keychain) + password = pwd_from_keychain or getpass() + result = validator(password) + # Password is bad: + if not result and pwd_from_keychain: + set_keychain(keychain, None) + attempt = 1 + while not result and attempt < max_attempts: + prompt("Wrong password, try again.") + password = getpass() + result = validator(password) + attempt += 1 + if result: + return result + else: + prompt("Extremely wrong password.") + sys.exit(1) + +def get_keychain(journal_name): + return keyring.get_password('jrnl', journal_name) + +def set_keychain(journal_name, password): + if password is None: + try: + keyring.delete_password('jrnl', journal_name) + except: + pass + else: + keyring.set_password('jrnl', journal_name, password) def u(s): """Mock unicode function for python 2 and 3 compatibility.""" @@ -35,6 +66,11 @@ def py23_input(msg): STDERR.write(u(msg)) return STDIN.readline().strip() +def yesno(prompt, default=True): + prompt = prompt.strip() + (" [Yn]" if default else "[yN]") + raw = py23_input(prompt) + return {'y': True, 'n': False}.get(raw.lower(), default) + def get_local_timezone(): """Returns the Olson identifier of the local timezone. In a happy world, tzlocal.get_localzone would do this, but there's a bug on OS X diff --git a/requirements.txt b/requirements.txt index 999b9b05..64c1ed0e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pycrypto >= 2.6 argparse==1.2.1 tzlocal == 1.0 slugify==0.0.1 +keyring==3.0.5 diff --git a/setup.py b/setup.py index 7f7e5d6c..668a44d9 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,8 @@ setup( "pytz>=2013b", "tzlocal==1.0", "slugify>=0.0.1", - "colorama>=0.2.5" + "colorama>=0.2.5", + "keyring>=3.0.5" ] + [p for p, cond in conditional_dependencies.items() if cond], extras_require = { "encrypted": "pycrypto>=2.6" From 7e3faeef45259629567a7c840cfcfe113bc9622b Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 17 Oct 2013 16:00:27 -0700 Subject: [PATCH 82/91] Disambiguates jrnl.update_config and install.update_config --- jrnl/install.py | 6 +++--- jrnl/jrnl.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/jrnl/install.py b/jrnl/install.py index d09e5150..0ff6159b 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -1,7 +1,8 @@ #!/usr/bin/env python # encoding: utf-8 -import readline, glob +import readline +import glob import getpass try: import simplejson as json except ImportError: import json @@ -25,7 +26,6 @@ default_config = { }, 'editor': "", 'encrypt': False, - 'password': "", 'default_hour': 9, 'default_minute': 0, 'timeformat': "%Y-%m-%d %H:%M", @@ -35,7 +35,7 @@ default_config = { } -def update_config(config, config_path=os.path.expanduser("~/.jrnl_conf")): +def upgrade_config(config, config_path=os.path.expanduser("~/.jrnl_conf")): """Checks if there are keys missing in a given config dict, and if so, updates the config file accordingly. This essentially automatically ports jrnl installations if new config parameters are introduced in later versions.""" diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index eec9b227..d9337308 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -109,7 +109,7 @@ def touch_journal(filename): def update_config(config, new_config, scope): """Updates a config dict with new values - either global if scope is None - of config['journals'][scope] is just a string pointing to a journal file, + or config['journals'][scope] is just a string pointing to a journal file, or within the scope""" if scope and type(config['journals'][scope]) is dict: # Update to journal specific config['journals'][scope].update(new_config) @@ -127,7 +127,7 @@ def cli(manual_args=None): util.prompt("[There seems to be something wrong with your jrnl config at {0}: {1}]".format(CONFIG_PATH, e.message)) util.prompt("[Entry was NOT added to your journal]") sys.exit(1) - install.update_config(config, config_path=CONFIG_PATH) + install.upgrade_config(config, config_path=CONFIG_PATH) original_config = config.copy() # check if the configuration is supported by available modules From 68cd41de40b6013176a90e3e4b506cec92809a78 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 17 Oct 2013 16:00:45 -0700 Subject: [PATCH 83/91] Remove tests for having password in config --- features/data/configs/encrypted_with_pw.json | 14 -------------- features/encryption.feature | 6 ------ jrnl/util.py | 4 ++-- 3 files changed, 2 insertions(+), 22 deletions(-) delete mode 100644 features/data/configs/encrypted_with_pw.json diff --git a/features/data/configs/encrypted_with_pw.json b/features/data/configs/encrypted_with_pw.json deleted file mode 100644 index 1a277240..00000000 --- a/features/data/configs/encrypted_with_pw.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "default_hour": 9, - "timeformat": "%Y-%m-%d %H:%M", - "linewrap": 80, - "encrypt": true, - "editor": "", - "default_minute": 0, - "highlight": true, - "password": "bad doggie no biscuit", - "journals": { - "default": "features/journals/encrypted.journal" - }, - "tagsymbols": "@" -} diff --git a/features/encryption.feature b/features/encryption.feature index d134c3bb..f28660b6 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -6,11 +6,6 @@ Then we should see the message "Password" and the output should contain "2013-06-10 15:40 Life is good" - Scenario: Loading an encrypted journal with password in config - Given we use the config "encrypted_with_pw.json" - When we run "jrnl -n 1" - Then the output should contain "2013-06-10 15:40 Life is good" - Scenario: Decrypting a journal Given we use the config "encrypted.json" When we run "jrnl --decrypt" and enter "bad doggie no biscuit" @@ -26,4 +21,3 @@ When we run "jrnl -n 1" and enter "swordfish" Then we should see the message "Password" and the output should contain "2013-06-10 15:40 Life is good" - diff --git a/jrnl/util.py b/jrnl/util.py index e0667cab..ddb4da59 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -49,7 +49,7 @@ def set_keychain(journal_name, password): keyring.delete_password('jrnl', journal_name) except: pass - else: + elif not TEST: keyring.set_password('jrnl', journal_name, password) def u(s): @@ -69,7 +69,7 @@ def py23_input(msg): def yesno(prompt, default=True): prompt = prompt.strip() + (" [Yn]" if default else "[yN]") raw = py23_input(prompt) - return {'y': True, 'n': False}.get(raw.lower(), default) + return {'y': True, 'n': False}.get(raw.lower(), default) def get_local_timezone(): """Returns the Olson identifier of the local timezone. From 50036e2ee1ef8033c38acc83d47b3abc77760601 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 17 Oct 2013 16:04:34 -0700 Subject: [PATCH 84/91] Only updates config locally on encrypting and decrypting --- jrnl/jrnl.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index d9337308..b82dd54c 100755 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -107,12 +107,15 @@ def touch_journal(filename): util.prompt("[Journal created at {0}]".format(filename)) open(filename, 'a').close() -def update_config(config, new_config, scope): +def update_config(config, new_config, scope, force_local=False): """Updates a config dict with new values - either global if scope is None or config['journals'][scope] is just a string pointing to a journal file, or within the scope""" if scope and type(config['journals'][scope]) is dict: # Update to journal specific config['journals'][scope].update(new_config) + elif scope and force_local: # Convert to dict + config['journals'][scope] = {"journal": config['journals'][scope]} + config['journals'][scope].update(new_config) else: config.update(new_config) @@ -206,14 +209,14 @@ def cli(manual_args=None): encrypt(journal, filename=args.encrypt) # Not encrypting to a separate file: update config! if not args.encrypt: - update_config(original_config, {"encrypt": True}, journal_name) + update_config(original_config, {"encrypt": True}, journal_name, force_local=True) install.save_config(original_config, config_path=CONFIG_PATH) elif args.decrypt is not False: decrypt(journal, filename=args.decrypt) # Not decrypting to a separate file: update config! if not args.decrypt: - update_config(original_config, {"encrypt": False}, journal_name) + update_config(original_config, {"encrypt": False}, journal_name, force_local=True) install.save_config(original_config, config_path=CONFIG_PATH) elif args.delete_last: From ed66d76db2ac61f97b78a833b0d3f1f3875ff6bd Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Thu, 17 Oct 2013 16:26:49 -0700 Subject: [PATCH 85/91] Tests for storing password in keychain --- features/data/configs/multiple.json | 1 + features/encryption.feature | 13 +++++++++++-- features/steps/core.py | 26 +++++++++++++++++++------- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/features/data/configs/multiple.json b/features/data/configs/multiple.json index af7a3e15..980c9353 100644 --- a/features/data/configs/multiple.json +++ b/features/data/configs/multiple.json @@ -9,6 +9,7 @@ "password": "", "journals": { "default": "features/journals/simple.journal", + "simple": "features/journals/simple.journal", "work": "features/journals/work.journal", "ideas": "features/journals/nothing.journal" }, diff --git a/features/encryption.feature b/features/encryption.feature index f28660b6..72749f23 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -9,15 +9,24 @@ Scenario: Decrypting a journal Given we use the config "encrypted.json" When we run "jrnl --decrypt" and enter "bad doggie no biscuit" + Then the config for journal "default" should have "encrypt" set to "bool:False" Then we should see the message "Journal decrypted" and the journal should have 2 entries - and the config should have "encrypt" set to "bool:False" Scenario: Encrypting a journal Given we use the config "basic.json" When we run "jrnl --encrypt" and enter "swordfish" Then we should see the message "Journal encrypted" - and the config should have "encrypt" set to "bool:True" + and the config for journal "default" should have "encrypt" set to "bool:True" When we run "jrnl -n 1" and enter "swordfish" Then we should see the message "Password" and the output should contain "2013-06-10 15:40 Life is good" + + Scenario: Storing a password in Keychain + Given we use the config "multiple.json" + When we run "jrnl simple --encrypt" and enter "sabertooth" + When we set the keychain password of "simple" to "sabertooth" + Then the config for journal "simple" should have "encrypt" set to "bool:True" + When we run "jrnl simple -n 1" + Then we should not see the message "Password" + and the output should contain "2013-06-10 15:40 Life is good" diff --git a/features/steps/core.py b/features/steps/core.py index da39387f..83a14017 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -5,6 +5,7 @@ import os import sys import json import pytz +import keyring try: from io import StringIO except ImportError: @@ -34,11 +35,11 @@ def read_journal(journal_name="default"): def open_journal(journal_name="default"): with open(jrnl.CONFIG_PATH) as config_file: config = json.load(config_file) - journals = config['journals'] - if type(journals) is dict: # We can override the default config on a by-journal basis - config['journal'] = journals.get(journal_name) - else: # But also just give them a string to point to the journal file - config['journal'] = journal + journal_conf = config['journals'][journal_name] + if type(journal_conf) is dict: # We can override the default config on a by-journal basis + config.update(journal_conf) + else: # But also just give them a string to point to the journal file + config['journal'] = journal_conf return Journal.Journal(**config) @given('we use the config "{config_file}"') @@ -68,6 +69,10 @@ def run(context, command): except SystemExit as e: context.exit_status = e.code +@when('we set the keychain password of "{journal}" to "{password}"') +def set_keychain(context, journal, password): + keyring.set_password('jrnl', journal, password) + @then('we should get an error') def has_error(context): assert context.exit_status != 0, context.exit_status @@ -134,6 +139,11 @@ def check_message(context, text): out = context.messages.getvalue() assert text in out, [text, out] +@then('we should not see the message "{text}"') +def check_not_message(context, text): + out = context.messages.getvalue() + assert text not in out, [text, out] + @then('the journal should contain "{text}"') @then('journal "{journal_name}" should contain "{text}"') def check_journal_content(context, text, journal_name="default"): @@ -148,7 +158,8 @@ def journal_doesnt_exist(context, journal_name="default"): assert not os.path.exists(journal_path) @then('the config should have "{key}" set to "{value}"') -def config_var(context, key, value): +@then('the config for journal "{journal}" should have "{key}" set to "{value}"') +def config_var(context, key, value, journal=None): t, value = value.split(":") value = { "bool": lambda v: v.lower() == "true", @@ -157,6 +168,8 @@ def config_var(context, key, value): }[t](value) with open(jrnl.CONFIG_PATH) as config_file: config = json.load(config_file) + if journal: + config = config["journals"][journal] assert key in config assert config[key] == value @@ -171,4 +184,3 @@ def check_journal_content(context, number, journal_name="default"): @then('fail') def debug_fail(context): assert False - From 7aa252c0ab2775ac7425e9141e6a4d6cc815886c Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 20 Oct 2013 13:25:42 -0700 Subject: [PATCH 86/91] User plain text keyring for testing --- features/steps/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/features/steps/core.py b/features/steps/core.py index 83a14017..c66a921f 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -6,6 +6,7 @@ import sys import json import pytz import keyring +keyring.set_keyring(keyring.backends.file.PlaintextKeyring()) try: from io import StringIO except ImportError: From 360580dbc7eb4a753675a784dae3521f56309066 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 20 Oct 2013 13:30:27 -0700 Subject: [PATCH 87/91] Updates readme --- README.md | 1 - jrnl/__init__.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 90f896d5..77ef006f 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,6 @@ The configuration file is a simple JSON file with the following options. - `journals`: paths to your journal files - `editor`: if set, executes this command to launch an external editor for writing your entries, e.g. `vim` or `subl -w` (note the `-w` flag to make sure _jrnl_ waits for Sublime Text to close the file before writing into the journal). - `encrypt`: if `true`, encrypts your journal using AES. -- `password`: you may store the password you used to encrypt your journal in plaintext here. This is useful if your journal file lives in an unsecure space (ie. your Dropbox), but the config file itself is more or less safe. - `tagsymbols`: Symbols to be interpreted as tags. (__See note below__) - `default_hour` and `default_minute`: if you supply a date, such as `last thursday`, but no specific time, the entry will be created at this time - `timeformat`: how to format the timestamps in your journal, see the [python docs](http://docs.python.org/library/time.html#time.strftime) for reference diff --git a/jrnl/__init__.py b/jrnl/__init__.py index 6124d668..4d384eb8 100644 --- a/jrnl/__init__.py +++ b/jrnl/__init__.py @@ -7,7 +7,7 @@ jrnl is a simple journal application for your command line. """ __title__ = 'jrnl' -__version__ = '1.6.0' +__version__ = '1.6.0-dev' __author__ = 'Manuel Ebert' __license__ = 'MIT License' __copyright__ = 'Copyright 2013 Manuel Ebert' From aac68db995d3a95c118ed130aa44b000bc554a53 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 20 Oct 2013 13:42:55 -0700 Subject: [PATCH 88/91] Only soft-deprecate passwords in config --- jrnl/Journal.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/jrnl/Journal.py b/jrnl/Journal.py index 116b6c9a..ecc9a4d7 100644 --- a/jrnl/Journal.py +++ b/jrnl/Journal.py @@ -99,14 +99,21 @@ class Journal(object): Entries have the form (date, title, body).""" filename = filename or self.config['journal'] - def validate_password(journal, password): - self.make_key(password) - return self._decrypt(journal) if self.config['encrypt']: with open(filename, "rb") as f: journal_encrypted = f.read() - journal = util.get_password(keychain=self.name, validator=partial(validate_password, journal_encrypted)) + + def validate_password(password): + self.make_key(password) + return self._decrypt(journal_encrypted) + + # Soft-deprecated: + journal = None + if 'password' in self.config: + journal = validate_password(self.config['password']) + if not journal: + journal = util.get_password(keychain=self.name, validator=validate_password) else: with codecs.open(filename, "r", "utf-8") as f: journal = f.read() From 36b375831ac703ef47e019e43c0a4057524cc830 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 20 Oct 2013 13:43:04 -0700 Subject: [PATCH 89/91] Tests for soft-deprecating passwords in config --- features/data/configs/encrypted_with_pw.json | 14 ++++++++++++++ features/encryption.feature | 7 ++++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 features/data/configs/encrypted_with_pw.json diff --git a/features/data/configs/encrypted_with_pw.json b/features/data/configs/encrypted_with_pw.json new file mode 100644 index 00000000..1a277240 --- /dev/null +++ b/features/data/configs/encrypted_with_pw.json @@ -0,0 +1,14 @@ +{ + "default_hour": 9, + "timeformat": "%Y-%m-%d %H:%M", + "linewrap": 80, + "encrypt": true, + "editor": "", + "default_minute": 0, + "highlight": true, + "password": "bad doggie no biscuit", + "journals": { + "default": "features/journals/encrypted.journal" + }, + "tagsymbols": "@" +} diff --git a/features/encryption.feature b/features/encryption.feature index 72749f23..43d07c26 100644 --- a/features/encryption.feature +++ b/features/encryption.feature @@ -1,4 +1,4 @@ - Feature: Multiple journals + Feature: Encrypted journals Scenario: Loading an encrypted journal Given we use the config "encrypted.json" @@ -22,6 +22,11 @@ Then we should see the message "Password" and the output should contain "2013-06-10 15:40 Life is good" + Scenario: Loading an encrypted journal with password in config + Given we use the config "encrypted_with_pw.json" + When we run "jrnl -n 1" + Then the output should contain "2013-06-10 15:40 Life is good" + Scenario: Storing a password in Keychain Given we use the config "multiple.json" When we run "jrnl simple --encrypt" and enter "sabertooth" From 0d451be033fc997bac4d5f231d87311f7376e5b2 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Sun, 20 Oct 2013 14:03:27 -0700 Subject: [PATCH 90/91] Delete tmp_log.bak --- tmp_log.bak | 1 - 1 file changed, 1 deletion(-) delete mode 100644 tmp_log.bak diff --git a/tmp_log.bak b/tmp_log.bak deleted file mode 100644 index 3d3a44c9..00000000 --- a/tmp_log.bak +++ /dev/null @@ -1 +0,0 @@ - From f9f20fb4ad6fb2dd4fa11ed595dc40d06da44d88 Mon Sep 17 00:00:00 2001 From: Manuel Ebert Date: Mon, 21 Oct 2013 14:36:31 -0700 Subject: [PATCH 91/91] Better prompts for y/n questions --- jrnl/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jrnl/util.py b/jrnl/util.py index ddb4da59..7ae9d4df 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -67,7 +67,7 @@ def py23_input(msg): return STDIN.readline().strip() def yesno(prompt, default=True): - prompt = prompt.strip() + (" [Yn]" if default else "[yN]") + prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]") raw = py23_input(prompt) return {'y': True, 'n': False}.get(raw.lower(), default)