From 11f732c9f2644302136ee90176ad7816bd596895 Mon Sep 17 00:00:00 2001
From: Zhiming Wang <zmwangx@gmail.com>
Date: Tue, 5 May 2015 22:08:54 -0700
Subject: 20150505 Graceful handling of SIGINT when using Python's
 multiprocessing.Process

Also implemented the "touch" action in pyblog, as well as wrote a
README.md for the source branch. And some other minor patching.
---
 pyblog | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 105 insertions(+), 21 deletions(-)

(limited to 'pyblog')

diff --git a/pyblog b/pyblog
index cab2972c..ce5c7986 100755
--- a/pyblog
+++ b/pyblog
@@ -5,6 +5,7 @@
 import argparse
 from contextlib import contextmanager
 import datetime
+import fileinput
 import io
 import http.client
 import http.server
@@ -96,6 +97,15 @@ def init_colorama():
     colorama.deinit()
 
 
+def current_datetime():
+    """Return the current datetime, complete with tzinfo.
+
+    Precision is one second. Timezone is the local timezone.
+    """
+    return datetime.datetime.fromtimestamp(round(time.time()),
+                                           dateutil.tz.tzlocal())
+
+
 class AtomFeed(object):
     """Class for storing atom:feed date and metadata."""
 
@@ -256,9 +266,8 @@ def generate_index_and_feed():
     # update time will be set after everthing finishes
 
     postspath = os.path.join(BUILDDIR, "blog")
-    # traverse all posts in reverse time order
-    for name in sorted(os.listdir(postspath), reverse=True):
-        if re.match(r"^(\d{4})-(\d{2})-(\d{2}).*\.html", name):
+    for name in os.listdir(postspath):
+        if re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}.*\.html", name):
             htmlpath = os.path.join(postspath, name)
             entry = AtomEntry()
             with open(htmlpath, encoding="utf-8") as htmlfile:
@@ -287,11 +296,12 @@ def generate_index_and_feed():
                 entry.content.append(cdata(entry.content_html))
                 entry.assemble_entry()
                 feed.entries.append(entry)
+    # sort entries by reverse chronological order
+    feed.entries.sort(key=lambda entry: entry.updated_datetime, reverse=True)
 
     generate_index(feed)
 
-    feed.updated_datetime = datetime.datetime.fromtimestamp(round(time.time()),
-                                                            dateutil.tz.tzlocal())
+    feed.updated_datetime = current_datetime()
     feed.updated = ET.Element("updated")
     feed.updated.text = feed.updated_datetime.isoformat()
 
@@ -440,8 +450,7 @@ def new_post(args):
 
     """
     title = args.title
-    date = datetime.datetime.fromtimestamp(round(time.time()),
-                                           dateutil.tz.tzlocal())
+    date = current_datetime()
     filename_date = date.strftime("%Y-%m-%d")
     iso_date = date.isoformat()
     display_date = "%s %d, %d" % (date.strftime("%B"), date.day, date.year)
@@ -464,6 +473,67 @@ def new_post(args):
     return 0
 
 
+def touch(args):
+    """Update the timestamp of a post to the current time."""
+    filename = os.path.basename(args.filename)
+    fullpath = os.path.join(SOURCEDIR, "blog", filename)
+    if not os.path.exists(fullpath):
+        sys.stderr.write("%serror: post %s not found %s\n" %
+                         (RED, fullpath, RESET))
+        return 1
+    filename_prefix_re = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}")
+    if not filename_prefix_re.match(filename):
+        sys.stderr.write(RED)
+        sys.stderr.write("error: post %s is not a valid post\n" % filename)
+        sys.stderr.write("error: the filename of a valid post begins with "
+                         "a date in the form xxxx-xx-xx\n")
+        sys.stderr.write(RESET)
+        return 1
+
+    # update timestamp in the metadata section of the post
+    whatchanged = io.StringIO()
+    date = current_datetime()
+    iso_date = date.isoformat()
+    display_date = "%s %d, %d" % (date.strftime("%B"), date.day, date.year)
+    filename_date = date.strftime("%Y-%m-%d")
+    with fileinput.input(files=(fullpath), inplace=True) as lines:
+        meta_fences = 0
+        for line in lines:
+            if line.startswith("---"):
+                meta_fences += 1
+                sys.stdout.write(line)
+                continue
+            if meta_fences >= 2:
+                # already went past the metadata section
+                sys.stdout.write(line)
+                continue
+
+            if line.startswith("date: "):
+                updated_line = "date: %s\n" % iso_date
+                sys.stdout.write(updated_line)
+                whatchanged.write("-%s+%s\n" % (line, updated_line))
+                continue
+
+            if line.startswith("date-display: "):
+                updated_line = "date-display: %s\n" % display_date
+                sys.stdout.write(updated_line)
+                whatchanged.write("-%s+%s\n" % (line, updated_line))
+                continue
+
+            sys.stdout.write(line)
+
+    sys.stderr.write("\nchangeset:\n\n%s" % whatchanged.getvalue())
+    whatchanged.close()
+
+    # check if the file needs to be renamed
+    new_filename = filename_prefix_re.sub(filename_date, filename)
+    if new_filename != filename:
+        new_fullpath = os.path.join(SOURCEDIR, "blog", new_filename)
+        os.rename(fullpath, new_fullpath)
+        sys.stderr.write("renamed to %s\n" % new_fullpath)
+    return 0
+
+
 def deploy(args):
     """Deploys build directory to origin/master without regenerating.
 
@@ -505,7 +575,7 @@ def deploy(args):
                 sys.stderr.write("Please answer yes or no.\n")
         if abort:
             sys.stderr.write("%saborting deployment%s\n" % (RED, RESET))
-            exit(1)
+            return 1
 
     # extract latest commit on the source branch
     source_commit = subprocess.check_output(
@@ -518,7 +588,7 @@ def deploy(args):
     # extract updated time from atom.xml
     if not os.path.exists("atom.xml"):
         sys.stderr.write("atom.xml not found, cannot deploy\naborting\n")
-        exit(1)
+        return 1
     atomxml = ET.parse("atom.xml").getroot()
     updated = atomxml.find('{http://www.w3.org/2005/Atom}updated').text
 
@@ -535,7 +605,7 @@ def deploy(args):
                                "--message=%s" % commit_message])
     except subprocess.CalledProcessError:
         sys.stderr.write("\n%serror: git commit failed%s\n" % (RED, RESET))
-        exit(1)
+        return 1
 
     # check dirty status
     dirty = subprocess.check_output(["git", "status", "--porcelain"])
@@ -545,7 +615,7 @@ def deploy(args):
                          "build directory still dirty\n")
         sys.stderr.write("error: please manually inspect what was left out\n")
         sys.stderr.write(RESET)
-        exit(1)
+        return 1
 
     # push to origin/master
     sys.stderr.write("%scommand: git push origin master%s\n" % (BLUE, RESET))
@@ -553,7 +623,7 @@ def deploy(args):
         subprocess.check_call(["git", "push", "origin", "master"])
     except subprocess.CalledProcessError:
         sys.stderr.write("\n%serror: git push failed%s\n" % (RED, RESET))
-        exit(1)
+        return 1
     return 0
 
 
@@ -641,6 +711,26 @@ def main():
     parser = argparse.ArgumentParser(description=description)
     subparsers = parser.add_subparsers()
 
+    parser_new_post = subparsers.add_parser(
+        "new_post", aliases=["n", "new"],
+        description="Create a new post with metadata pre-filled.")
+    parser_new_post.add_argument("title", help="title of the new post")
+    parser_new_post.set_defaults(func=new_post)
+
+    parser_new_post = subparsers.add_parser(
+        "touch", aliases=["t", "tou"],
+        description="""Touch an existing post, i.e., update its
+        timestamp to current time.  Why is this ever useful? Well, the
+        timestamp filled in by new_post is the time of creation, but one
+        might spend several hours after the creation of the file to
+        finish the post. Sometimes the post is even created on one day
+        and finished on another (say created at 11pm and finished at
+        1am). Therefore, one may want to retouch the timestamp before
+        publishing.""")
+    parser_new_post.add_argument("filename",
+        help="path or basename of the source file, e.g., 2015-05-05-new-blog-new-start.md")
+    parser_new_post.set_defaults(func=touch)
+
     parser_generate = subparsers.add_parser(
         "generate", aliases=["g", "gen"],
         description="Generate new or changed objects.")
@@ -652,10 +742,9 @@ def main():
     parser_regenerate.set_defaults(func=regenerate)
 
     parser_new_post = subparsers.add_parser(
-        "new_post", aliases=["n", "new"],
-        description="Create a new post with metadata pre-filled.")
-    parser_new_post.add_argument("title", help="title of the new post")
-    parser_new_post.set_defaults(func=new_post)
+        "preview", aliases=["p", "pre"],
+        description="Serve the blog locally and auto regenerate upon changes.")
+    parser_new_post.set_defaults(func=preview)
 
     parser_new_post = subparsers.add_parser(
         "deploy", aliases=["d", "dep"],
@@ -667,11 +756,6 @@ def main():
         description="Rebuild entire blog and deploy build/ to origin/master.")
     parser_new_post.set_defaults(func=gen_deploy)
 
-    parser_new_post = subparsers.add_parser(
-        "preview", aliases=["p", "pre"],
-        description="Serve the blog locally and auto regenerate upon changes.")
-    parser_new_post.set_defaults(func=preview)
-
     with init_colorama():
         args = parser.parse_args()
         returncode = args.func(args)
-- 
cgit v1.2.1