diff frontends/src/jp/cmd_blog.py @ 1868:28b29381db75

jp (blog/edit): added metadata handling through a .json file (named like content temporary file, but with extension replaced by "_metadata.json"). Modification to this file before the end of edition will be taken into account.
author Goffi <goffi@goffi.org>
date Wed, 02 Mar 2016 19:18:14 +0100
parents 397ef87958b9
children c25077c87b1d
line wrap: on
line diff
--- a/frontends/src/jp/cmd_blog.py	Tue Mar 01 16:36:16 2016 +0100
+++ b/frontends/src/jp/cmd_blog.py	Wed Mar 02 19:18:14 2016 +0100
@@ -38,6 +38,8 @@
                }
 CONF_SYNTAX_EXT = 'syntax_ext_dict'
 BLOG_TMP_DIR="blog"
+# key to remove from metadata tmp file if they exist
+KEY_TO_REMOVE_METADATA = ('id','content', 'content_rich', 'content_xhtml', 'comments_node', 'comments_service')
 
 URL_REDIRECT_PREFIX = 'url_redirect_'
 
@@ -54,6 +56,12 @@
         self.parser.add_argument("--no-comment", action='store_true', help=_(u"disable comments"))
 
     def getTmpFile(self, sat_conf, tmp_suff):
+        """Create a temporary file to received blog item body
+
+        @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration
+        @param tmp_suff (str): suffix to use for the filename
+        @return (tuple(file, str)): opened (w+b) file object and file path
+        """
         local_dir = config.getConfig(sat_conf, '', 'local_dir', Exception)
         tmp_dir = os.path.join(local_dir, BLOG_TMP_DIR)
         if not os.path.exists(tmp_dir):
@@ -63,63 +71,109 @@
                 self.disp(u"Can't create {path} directory: {reason}".format(
                     path=tmp_dir, reason=e), error=True)
                 self.host.quit(1)
-
         try:
-            return tempfile.mkstemp(suffix=tmp_suff,
+            fd, path = tempfile.mkstemp(suffix=tmp_suff,
                 prefix=time.strftime('blog_%Y-%m-%d_%H:%M:%S_'),
                 dir=tmp_dir, text=True)
+            return os.fdopen(fd, 'w+b'), path
         except OSError as e:
             self.disp(u"Can't create temporary file: {reason}".format(reason=e), error=True)
             self.host.quit(1)
 
-    def edit(self, sat_conf, tmp_file, tmp_file_obj, item_id=None):
+    def buildMetadataFile(self, tmp_file_path, mb_data=None):
+        """Build a metadata file using json
+
+        The file is named after tmp_file_path, with extension replaced by _metadata.json
+        @param tmp_file_path(str): path to the temporary file which will contain the body
+        @param mb_data(dict, None): microblog metadata (for existing items)
+        @return (tuple[dict, str]): merged metadata put originaly in metadata file
+            and path to temporary metadata file
+        """
+        # we first construct metadata from edited item ones and CLI argumments
+        mb_data = {} if mb_data is None else mb_data.copy()
+        for key in KEY_TO_REMOVE_METADATA:
+            try:
+                del mb_data[key]
+            except KeyError:
+                pass
+        mb_data['allow_comments'] = C.boolConst(not self.args.no_comment)
+        if self.args.tag:
+            common.iter2dict('tag', self.args.tag, mb_data)
+        if self.args.title is not None:
+            mb_data['title'] = self.args.title
+
+        # the we create the file and write metadata there, as JSON dict
+        meta_file_path = os.path.splitext(tmp_file_path)[0] + '_metadata.json'
+        # XXX: if we port jp one day on Windows, O_BINARY may need to be added here
+        if os.path.exists(meta_file_path):
+            self.disp(u"metadata file {} already exists, this should not happen! Cancelling...", error=True)
+        with os.fdopen(os.open(meta_file_path, os.O_RDWR | os.O_CREAT ,0o600), 'w+b') as f:
+            # we need to use an intermediate unicode buffer to write to the file unicode without escaping characters
+            unicode_dump = json.dumps(mb_data, ensure_ascii=False, indent=4, separators=(',', ': '), sort_keys=True)
+            f.write(unicode_dump.encode('utf-8'))
+
+        return mb_data, meta_file_path
+
+    def edit(self, sat_conf, tmp_file_path, tmp_file_obj, mb_data=None):
         """Edit the file contening the content using editor, and publish it"""
-        # we first calculate hash to check for modifications
+        item_ori_mb_data = mb_data
+        # we first create metadata file
+        meta_ori, meta_file_path = self.buildMetadataFile(tmp_file_path, item_ori_mb_data)
+
+        # then we calculate hashes to check for modifications
         import hashlib
         tmp_file_obj.seek(0)
-        ori_hash = hashlib.sha1(tmp_file_obj.read()).digest()
+        tmp_ori_hash = hashlib.sha1(tmp_file_obj.read()).digest()
         tmp_file_obj.close()
 
-        # the we launch editor
+        # then we launch editor
         editor = config.getConfig(sat_conf, 'jp', 'editor') or os.getenv('EDITOR', 'vi')
-        editor_exit = subprocess.call([editor, tmp_file])
+        editor_exit = subprocess.call([editor, tmp_file_path])
 
         # we send the file if edition was a success
         if editor_exit != 0:
             self.disp(u"Editor exited with an error code, so temporary file has not be deleted, and blog item is not published.\nTou can find temporary file at {path}".format(
-                path=tmp_file), error=True)
+                path=tmp_file_path), error=True)
         else:
-            with open(tmp_file, 'rb') as f:
-                content = f.read()
+            try:
+                with open(tmp_file_path, 'rb') as f:
+                    content = f.read()
+                with open(meta_file_path, 'rb') as f:
+                    mb_data = json.load(f)
+            except OSError:
+                self.disp(u"Can write files at {file_path} and/or {meta_path}, have they been deleted? Cancelling edition".format(
+                    file_path=tmp_file_path, meta_path=meta_file_path), error=True)
+                self.host.quit(1)
+            except ValueError:
+                self.disp(u"Can't parse metadata, please check it is correct JSON format. Cancelling edition.\n" +
+                    "You can find tmp file at {file_path} and temporary meta file at {meta_path}.".format(
+                    file_path=tmp_file_path, meta_path=meta_file_path), error=True)
+                self.host.quit(1)
 
             if len(content) == 0:
                 self.disp(u"Content is empty, cancelling the blog edition")
 
             # time to re-check the hash
-            elif ori_hash == hashlib.sha1(content).digest():
+            elif (tmp_ori_hash == hashlib.sha1(content).digest() and
+                  meta_ori == mb_data):
                 self.disp(u"The content has not been modified, cancelling the blog edition")
 
             else:
                 # we can now send the blog
-                mb_data = {
-                    'content_rich': content.decode('utf-8'),
-                    'allow_comments': C.boolConst(not self.args.no_comment),
-                    }
-                if item_id:
-                    mb_data['id'] = item_id
-                if self.args.tag:
-                    common.iter2dict('tag', self.args.tag, mb_data)
+                mb_data['content_rich'] =  content.decode('utf-8')
 
-                if self.args.title is not None:
-                    mb_data['title'] = self.args.title
+                if item_ori_mb_data is not None:
+                    mb_data['id'] = item_ori_mb_data['id']
+
                 try:
                     self.host.bridge.mbSend('', '', mb_data, self.profile)
                 except Exception as e:
-                    self.disp(u"Error while sending your blog, the temporary file has been kept at {path}: {reason}".format(
-                        path=tmp_file, reason=e), error=True)
+                    self.disp(u"Error while sending your blog, the temporary files have been kept at {file_path} and {meta_path}: {reason}".format(
+                        file_path=tmp_file_path, meta_path=meta_file_path, reason=e), error=True)
                     self.host.quit(1)
 
-            os.unlink(tmp_file)
+            os.unlink(tmp_file_path)
+            os.unlink(meta_file_path)
 
     def start(self):
         # we get current syntax to determine file extension
@@ -131,12 +185,12 @@
 
         # we now create a temporary file
         tmp_suff = '.' + SYNTAX_EXT.get(current_syntax, SYNTAX_EXT[''])
-        fd, tmp_file = self.getTmpFile(sat_conf, tmp_suff)
+        tmp_file_obj, tmp_file_path = self.getTmpFile(sat_conf, tmp_suff)
 
         item_lower = self.args.item.lower()
         if item_lower == 'new':
             self.disp(u'Editing a new blog item', 2)
-            self.edit(sat_conf, tmp_file, os.fdopen(fd))
+            self.edit(sat_conf, tmp_file_path, tmp_file_obj)
         elif item_lower == 'last':
             self.disp(u'Editing last published item', 2)
             try:
@@ -148,10 +202,9 @@
             content = mb_data['content_xhtml']
             if current_syntax != 'XHTML':
                 content = self.host.bridge.syntaxConvert(content, 'XHTML', current_syntax, False, self.profile)
-            f = os.fdopen(fd, 'w+b')
-            f.write(content.encode('utf-8'))
-            f.seek(0)
-            self.edit(sat_conf, tmp_file, f, mb_data['id'])
+            tmp_file_obj.write(content.encode('utf-8'))
+            tmp_file_obj.seek(0)
+            self.edit(sat_conf, tmp_file_path, tmp_file_obj, mb_data=mb_data)
 
 
 class Import(base.CommandAnswering):