comparison 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
comparison
equal deleted inserted replaced
1867:47108a4f3a70 1868:28b29381db75
36 "XHTML": "xhtml", 36 "XHTML": "xhtml",
37 "markdown": "md" 37 "markdown": "md"
38 } 38 }
39 CONF_SYNTAX_EXT = 'syntax_ext_dict' 39 CONF_SYNTAX_EXT = 'syntax_ext_dict'
40 BLOG_TMP_DIR="blog" 40 BLOG_TMP_DIR="blog"
41 # key to remove from metadata tmp file if they exist
42 KEY_TO_REMOVE_METADATA = ('id','content', 'content_rich', 'content_xhtml', 'comments_node', 'comments_service')
41 43
42 URL_REDIRECT_PREFIX = 'url_redirect_' 44 URL_REDIRECT_PREFIX = 'url_redirect_'
43 45
44 46
45 class Edit(base.CommandBase): 47 class Edit(base.CommandBase):
52 self.parser.add_argument("-T", '--title', type=base.unicode_decoder, help=_(u"Title of the item")) 54 self.parser.add_argument("-T", '--title', type=base.unicode_decoder, help=_(u"Title of the item"))
53 self.parser.add_argument("-t", '--tag', type=base.unicode_decoder, action='append', help=_(u"tag (category) of your item")) 55 self.parser.add_argument("-t", '--tag', type=base.unicode_decoder, action='append', help=_(u"tag (category) of your item"))
54 self.parser.add_argument("--no-comment", action='store_true', help=_(u"disable comments")) 56 self.parser.add_argument("--no-comment", action='store_true', help=_(u"disable comments"))
55 57
56 def getTmpFile(self, sat_conf, tmp_suff): 58 def getTmpFile(self, sat_conf, tmp_suff):
59 """Create a temporary file to received blog item body
60
61 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration
62 @param tmp_suff (str): suffix to use for the filename
63 @return (tuple(file, str)): opened (w+b) file object and file path
64 """
57 local_dir = config.getConfig(sat_conf, '', 'local_dir', Exception) 65 local_dir = config.getConfig(sat_conf, '', 'local_dir', Exception)
58 tmp_dir = os.path.join(local_dir, BLOG_TMP_DIR) 66 tmp_dir = os.path.join(local_dir, BLOG_TMP_DIR)
59 if not os.path.exists(tmp_dir): 67 if not os.path.exists(tmp_dir):
60 try: 68 try:
61 os.makedirs(tmp_dir) 69 os.makedirs(tmp_dir)
62 except OSError as e: 70 except OSError as e:
63 self.disp(u"Can't create {path} directory: {reason}".format( 71 self.disp(u"Can't create {path} directory: {reason}".format(
64 path=tmp_dir, reason=e), error=True) 72 path=tmp_dir, reason=e), error=True)
65 self.host.quit(1) 73 self.host.quit(1)
66
67 try: 74 try:
68 return tempfile.mkstemp(suffix=tmp_suff, 75 fd, path = tempfile.mkstemp(suffix=tmp_suff,
69 prefix=time.strftime('blog_%Y-%m-%d_%H:%M:%S_'), 76 prefix=time.strftime('blog_%Y-%m-%d_%H:%M:%S_'),
70 dir=tmp_dir, text=True) 77 dir=tmp_dir, text=True)
78 return os.fdopen(fd, 'w+b'), path
71 except OSError as e: 79 except OSError as e:
72 self.disp(u"Can't create temporary file: {reason}".format(reason=e), error=True) 80 self.disp(u"Can't create temporary file: {reason}".format(reason=e), error=True)
73 self.host.quit(1) 81 self.host.quit(1)
74 82
75 def edit(self, sat_conf, tmp_file, tmp_file_obj, item_id=None): 83 def buildMetadataFile(self, tmp_file_path, mb_data=None):
84 """Build a metadata file using json
85
86 The file is named after tmp_file_path, with extension replaced by _metadata.json
87 @param tmp_file_path(str): path to the temporary file which will contain the body
88 @param mb_data(dict, None): microblog metadata (for existing items)
89 @return (tuple[dict, str]): merged metadata put originaly in metadata file
90 and path to temporary metadata file
91 """
92 # we first construct metadata from edited item ones and CLI argumments
93 mb_data = {} if mb_data is None else mb_data.copy()
94 for key in KEY_TO_REMOVE_METADATA:
95 try:
96 del mb_data[key]
97 except KeyError:
98 pass
99 mb_data['allow_comments'] = C.boolConst(not self.args.no_comment)
100 if self.args.tag:
101 common.iter2dict('tag', self.args.tag, mb_data)
102 if self.args.title is not None:
103 mb_data['title'] = self.args.title
104
105 # the we create the file and write metadata there, as JSON dict
106 meta_file_path = os.path.splitext(tmp_file_path)[0] + '_metadata.json'
107 # XXX: if we port jp one day on Windows, O_BINARY may need to be added here
108 if os.path.exists(meta_file_path):
109 self.disp(u"metadata file {} already exists, this should not happen! Cancelling...", error=True)
110 with os.fdopen(os.open(meta_file_path, os.O_RDWR | os.O_CREAT ,0o600), 'w+b') as f:
111 # we need to use an intermediate unicode buffer to write to the file unicode without escaping characters
112 unicode_dump = json.dumps(mb_data, ensure_ascii=False, indent=4, separators=(',', ': '), sort_keys=True)
113 f.write(unicode_dump.encode('utf-8'))
114
115 return mb_data, meta_file_path
116
117 def edit(self, sat_conf, tmp_file_path, tmp_file_obj, mb_data=None):
76 """Edit the file contening the content using editor, and publish it""" 118 """Edit the file contening the content using editor, and publish it"""
77 # we first calculate hash to check for modifications 119 item_ori_mb_data = mb_data
120 # we first create metadata file
121 meta_ori, meta_file_path = self.buildMetadataFile(tmp_file_path, item_ori_mb_data)
122
123 # then we calculate hashes to check for modifications
78 import hashlib 124 import hashlib
79 tmp_file_obj.seek(0) 125 tmp_file_obj.seek(0)
80 ori_hash = hashlib.sha1(tmp_file_obj.read()).digest() 126 tmp_ori_hash = hashlib.sha1(tmp_file_obj.read()).digest()
81 tmp_file_obj.close() 127 tmp_file_obj.close()
82 128
83 # the we launch editor 129 # then we launch editor
84 editor = config.getConfig(sat_conf, 'jp', 'editor') or os.getenv('EDITOR', 'vi') 130 editor = config.getConfig(sat_conf, 'jp', 'editor') or os.getenv('EDITOR', 'vi')
85 editor_exit = subprocess.call([editor, tmp_file]) 131 editor_exit = subprocess.call([editor, tmp_file_path])
86 132
87 # we send the file if edition was a success 133 # we send the file if edition was a success
88 if editor_exit != 0: 134 if editor_exit != 0:
89 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( 135 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(
90 path=tmp_file), error=True) 136 path=tmp_file_path), error=True)
91 else: 137 else:
92 with open(tmp_file, 'rb') as f: 138 try:
93 content = f.read() 139 with open(tmp_file_path, 'rb') as f:
140 content = f.read()
141 with open(meta_file_path, 'rb') as f:
142 mb_data = json.load(f)
143 except OSError:
144 self.disp(u"Can write files at {file_path} and/or {meta_path}, have they been deleted? Cancelling edition".format(
145 file_path=tmp_file_path, meta_path=meta_file_path), error=True)
146 self.host.quit(1)
147 except ValueError:
148 self.disp(u"Can't parse metadata, please check it is correct JSON format. Cancelling edition.\n" +
149 "You can find tmp file at {file_path} and temporary meta file at {meta_path}.".format(
150 file_path=tmp_file_path, meta_path=meta_file_path), error=True)
151 self.host.quit(1)
94 152
95 if len(content) == 0: 153 if len(content) == 0:
96 self.disp(u"Content is empty, cancelling the blog edition") 154 self.disp(u"Content is empty, cancelling the blog edition")
97 155
98 # time to re-check the hash 156 # time to re-check the hash
99 elif ori_hash == hashlib.sha1(content).digest(): 157 elif (tmp_ori_hash == hashlib.sha1(content).digest() and
158 meta_ori == mb_data):
100 self.disp(u"The content has not been modified, cancelling the blog edition") 159 self.disp(u"The content has not been modified, cancelling the blog edition")
101 160
102 else: 161 else:
103 # we can now send the blog 162 # we can now send the blog
104 mb_data = { 163 mb_data['content_rich'] = content.decode('utf-8')
105 'content_rich': content.decode('utf-8'), 164
106 'allow_comments': C.boolConst(not self.args.no_comment), 165 if item_ori_mb_data is not None:
107 } 166 mb_data['id'] = item_ori_mb_data['id']
108 if item_id: 167
109 mb_data['id'] = item_id
110 if self.args.tag:
111 common.iter2dict('tag', self.args.tag, mb_data)
112
113 if self.args.title is not None:
114 mb_data['title'] = self.args.title
115 try: 168 try:
116 self.host.bridge.mbSend('', '', mb_data, self.profile) 169 self.host.bridge.mbSend('', '', mb_data, self.profile)
117 except Exception as e: 170 except Exception as e:
118 self.disp(u"Error while sending your blog, the temporary file has been kept at {path}: {reason}".format( 171 self.disp(u"Error while sending your blog, the temporary files have been kept at {file_path} and {meta_path}: {reason}".format(
119 path=tmp_file, reason=e), error=True) 172 file_path=tmp_file_path, meta_path=meta_file_path, reason=e), error=True)
120 self.host.quit(1) 173 self.host.quit(1)
121 174
122 os.unlink(tmp_file) 175 os.unlink(tmp_file_path)
176 os.unlink(meta_file_path)
123 177
124 def start(self): 178 def start(self):
125 # we get current syntax to determine file extension 179 # we get current syntax to determine file extension
126 current_syntax = self.host.bridge.getParamA("Syntax", "Composition", "value", self.profile) 180 current_syntax = self.host.bridge.getParamA("Syntax", "Composition", "value", self.profile)
127 self.disp(u"Current syntax: {}".format(current_syntax), 1) 181 self.disp(u"Current syntax: {}".format(current_syntax), 1)
129 # if there are user defined extension, we use them 183 # if there are user defined extension, we use them
130 SYNTAX_EXT.update(config.getConfig(sat_conf, 'jp', CONF_SYNTAX_EXT, {})) 184 SYNTAX_EXT.update(config.getConfig(sat_conf, 'jp', CONF_SYNTAX_EXT, {}))
131 185
132 # we now create a temporary file 186 # we now create a temporary file
133 tmp_suff = '.' + SYNTAX_EXT.get(current_syntax, SYNTAX_EXT['']) 187 tmp_suff = '.' + SYNTAX_EXT.get(current_syntax, SYNTAX_EXT[''])
134 fd, tmp_file = self.getTmpFile(sat_conf, tmp_suff) 188 tmp_file_obj, tmp_file_path = self.getTmpFile(sat_conf, tmp_suff)
135 189
136 item_lower = self.args.item.lower() 190 item_lower = self.args.item.lower()
137 if item_lower == 'new': 191 if item_lower == 'new':
138 self.disp(u'Editing a new blog item', 2) 192 self.disp(u'Editing a new blog item', 2)
139 self.edit(sat_conf, tmp_file, os.fdopen(fd)) 193 self.edit(sat_conf, tmp_file_path, tmp_file_obj)
140 elif item_lower == 'last': 194 elif item_lower == 'last':
141 self.disp(u'Editing last published item', 2) 195 self.disp(u'Editing last published item', 2)
142 try: 196 try:
143 mb_data = self.host.bridge.mbGet('', '', 1, [], {}, self.profile)[0][0] 197 mb_data = self.host.bridge.mbGet('', '', 1, [], {}, self.profile)[0][0]
144 except Exception as e: 198 except Exception as e:
146 self.host.quit(1) 200 self.host.quit(1)
147 201
148 content = mb_data['content_xhtml'] 202 content = mb_data['content_xhtml']
149 if current_syntax != 'XHTML': 203 if current_syntax != 'XHTML':
150 content = self.host.bridge.syntaxConvert(content, 'XHTML', current_syntax, False, self.profile) 204 content = self.host.bridge.syntaxConvert(content, 'XHTML', current_syntax, False, self.profile)
151 f = os.fdopen(fd, 'w+b') 205 tmp_file_obj.write(content.encode('utf-8'))
152 f.write(content.encode('utf-8')) 206 tmp_file_obj.seek(0)
153 f.seek(0) 207 self.edit(sat_conf, tmp_file_path, tmp_file_obj, mb_data=mb_data)
154 self.edit(sat_conf, tmp_file, f, mb_data['id'])
155 208
156 209
157 class Import(base.CommandAnswering): 210 class Import(base.CommandAnswering):
158 def __init__(self, host): 211 def __init__(self, host):
159 super(Import, self).__init__(host, 'import', use_progress=True, help=_(u'import an external blog')) 212 super(Import, self).__init__(host, 'import', use_progress=True, help=_(u'import an external blog'))