Mercurial > libervia-backend
comparison frontends/src/jp/cmd_blog.py @ 1872:df1ca137b0cb
jp (blog/edit): editor arguments can now be specified on sat.conf, and default on are applied for known editors:
- vim and gvim will open content and metadata file in a splitted window
- gvim use --nofork option
- installed a workaround for shlex.split not handling unicode before Python 2.7.3
- some fixes
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 03 Mar 2016 15:57:06 +0100 |
parents | 64a40adccba4 |
children | 6ec54626610c |
comparison
equal
deleted
inserted
replaced
1871:64a40adccba4 | 1872:df1ca137b0cb |
---|---|
20 | 20 |
21 import base | 21 import base |
22 from sat.core.i18n import _ | 22 from sat.core.i18n import _ |
23 from sat.core.constants import Const as C | 23 from sat.core.constants import Const as C |
24 from sat.tools import config | 24 from sat.tools import config |
25 from ConfigParser import NoSectionError, NoOptionError | |
25 import json | 26 import json |
26 import os.path | 27 import os.path |
27 import os | 28 import os |
28 import time | 29 import time |
29 import tempfile | 30 import tempfile |
30 import subprocess | 31 import subprocess |
32 import shlex | |
31 from sat.tools import common | 33 from sat.tools import common |
32 | 34 |
33 __commands__ = ["Blog"] | 35 __commands__ = ["Blog"] |
34 | 36 |
35 SYNTAX_EXT = { '': 'txt', # used when the syntax is not found | 37 # extensions to use with known syntaxes |
36 "XHTML": "xhtml", | 38 SYNTAX_EXT = { |
37 "markdown": "md" | 39 '': 'txt', # used when the syntax is not found |
38 } | 40 "XHTML": "xhtml", |
41 "markdown": "md" | |
42 } | |
43 | |
44 # defaut arguments used for some known editors | |
45 VIM_SPLIT_ARGS="-c 'vsplit|wincmd w|next|wincmd w'" | |
46 EDITOR_ARGS_MAGIC = { | |
47 'vim': VIM_SPLIT_ARGS + ' {content_file} {metadata_file}', | |
48 'gvim': VIM_SPLIT_ARGS + ' --nofork {content_file} {metadata_file}', | |
49 } | |
50 | |
39 CONF_SYNTAX_EXT = 'syntax_ext_dict' | 51 CONF_SYNTAX_EXT = 'syntax_ext_dict' |
40 BLOG_TMP_DIR="blog" | 52 BLOG_TMP_DIR="blog" |
41 # key to remove from metadata tmp file if they exist | 53 # key to remove from metadata tmp file if they exist |
42 KEY_TO_REMOVE_METADATA = ('id','content', 'content_xhtml', 'comments_node', 'comments_service') | 54 KEY_TO_REMOVE_METADATA = ('id','content', 'content_xhtml', 'comments_node', 'comments_service') |
43 | 55 |
78 return os.fdopen(fd, 'w+b'), path | 90 return os.fdopen(fd, 'w+b'), path |
79 except OSError as e: | 91 except OSError as e: |
80 self.disp(u"Can't create temporary file: {reason}".format(reason=e), error=True) | 92 self.disp(u"Can't create temporary file: {reason}".format(reason=e), error=True) |
81 self.host.quit(1) | 93 self.host.quit(1) |
82 | 94 |
83 def buildMetadataFile(self, tmp_file_path, mb_data=None): | 95 def buildMetadataFile(self, content_file_path, mb_data=None): |
84 """Build a metadata file using json | 96 """Build a metadata file using json |
85 | 97 |
86 The file is named after tmp_file_path, with extension replaced by _metadata.json | 98 The file is named after content_file_path, with extension replaced by _metadata.json |
87 @param tmp_file_path(str): path to the temporary file which will contain the body | 99 @param content_file_path(str): path to the temporary file which will contain the body |
88 @param mb_data(dict, None): microblog metadata (for existing items) | 100 @param mb_data(dict, None): microblog metadata (for existing items) |
89 @return (tuple[dict, str]): merged metadata put originaly in metadata file | 101 @return (tuple[dict, str]): merged metadata put originaly in metadata file |
90 and path to temporary metadata file | 102 and path to temporary metadata file |
91 """ | 103 """ |
92 # we first construct metadata from edited item ones and CLI argumments | 104 # we first construct metadata from edited item ones and CLI argumments |
101 common.iter2dict('tag', self.args.tag, mb_data) | 113 common.iter2dict('tag', self.args.tag, mb_data) |
102 if self.args.title is not None: | 114 if self.args.title is not None: |
103 mb_data['title'] = self.args.title | 115 mb_data['title'] = self.args.title |
104 | 116 |
105 # the we create the file and write metadata there, as JSON dict | 117 # 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' | 118 meta_file_path = os.path.splitext(content_file_path)[0] + '_metadata.json' |
107 # XXX: if we port jp one day on Windows, O_BINARY may need to be added here | 119 # 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): | 120 if os.path.exists(meta_file_path): |
109 self.disp(u"metadata file {} already exists, this should not happen! Cancelling...", error=True) | 121 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: | 122 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 | 123 # 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) | 124 unicode_dump = json.dumps(mb_data, ensure_ascii=False, indent=4, separators=(',', ': '), sort_keys=True) |
113 f.write(unicode_dump.encode('utf-8')) | 125 f.write(unicode_dump.encode('utf-8')) |
114 | 126 |
115 return mb_data, meta_file_path | 127 return mb_data, meta_file_path |
116 | 128 |
117 def edit(self, sat_conf, tmp_file_path, tmp_file_obj, mb_data=None): | 129 def edit(self, sat_conf, content_file_path, content_file_obj, mb_data=None): |
118 """Edit the file contening the content using editor, and publish it""" | 130 """Edit the file contening the content using editor, and publish it""" |
119 item_ori_mb_data = mb_data | 131 item_ori_mb_data = mb_data |
120 # we first create metadata file | 132 # we first create metadata file |
121 meta_ori, meta_file_path = self.buildMetadataFile(tmp_file_path, item_ori_mb_data) | 133 meta_ori, meta_file_path = self.buildMetadataFile(content_file_path, item_ori_mb_data) |
122 | 134 |
123 # then we calculate hashes to check for modifications | 135 # then we calculate hashes to check for modifications |
124 import hashlib | 136 import hashlib |
125 tmp_file_obj.seek(0) | 137 content_file_obj.seek(0) |
126 tmp_ori_hash = hashlib.sha1(tmp_file_obj.read()).digest() | 138 tmp_ori_hash = hashlib.sha1(content_file_obj.read()).digest() |
127 tmp_file_obj.close() | 139 content_file_obj.close() |
128 | 140 |
129 # then we launch editor | 141 # then we launch editor |
130 editor = config.getConfig(sat_conf, 'jp', 'editor') or os.getenv('EDITOR', 'vi') | 142 editor = config.getConfig(sat_conf, 'jp', 'editor') or os.getenv('EDITOR', 'vi') |
131 editor_exit = subprocess.call([editor, tmp_file_path]) | 143 try: |
144 # is there custom arguments in sat.conf ? | |
145 editor_args = config.getConfig(sat_conf, 'jp', 'blog_editor_args', Exception) | |
146 except (NoOptionError, NoSectionError): | |
147 # no, we check if we know the editor and have special arguments | |
148 editor_args = EDITOR_ARGS_MAGIC.get(os.path.basename(editor), '') | |
149 try: | |
150 # we split the arguments and add the known fields | |
151 # we split arguments first to avoid escaping issues in file names | |
152 args = [a.format(content_file=content_file_path, metadata_file=meta_file_path) for a in shlex.split(editor_args)] | |
153 except ValueError as e: | |
154 self.disp(u"Couldn't parse editor cmd [{cmd}]: {reason}".format(cmd=editor_args, reason=e)) | |
155 args = [] | |
156 if not args: | |
157 args = [content_file_path] | |
158 editor_exit = subprocess.call([editor] + args) | |
132 | 159 |
133 # we send the file if edition was a success | 160 # we send the file if edition was a success |
134 if editor_exit != 0: | 161 if editor_exit != 0: |
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( | 162 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( |
136 path=tmp_file_path), error=True) | 163 path=content_file_path), error=True) |
137 else: | 164 else: |
138 try: | 165 try: |
139 with open(tmp_file_path, 'rb') as f: | 166 with open(content_file_path, 'rb') as f: |
140 content = f.read() | 167 content = f.read() |
141 with open(meta_file_path, 'rb') as f: | 168 with open(meta_file_path, 'rb') as f: |
142 mb_data = json.load(f) | 169 mb_data = json.load(f) |
143 except OSError: | 170 except (OSError, IOError): |
144 self.disp(u"Can write files at {file_path} and/or {meta_path}, have they been deleted? Cancelling edition".format( | 171 self.disp(u"Can read files at {content_path} and/or {meta_path}, have they been deleted?\nCancelling edition".format( |
145 file_path=tmp_file_path, meta_path=meta_file_path), error=True) | 172 content_path=content_file_path, meta_path=meta_file_path), error=True) |
146 self.host.quit(1) | 173 self.host.quit(1) |
147 except ValueError: | 174 except ValueError: |
148 self.disp(u"Can't parse metadata, please check it is correct JSON format. Cancelling edition.\n" + | 175 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( | 176 "You can find tmp file at {content_path} and temporary meta file at {meta_path}.".format( |
150 file_path=tmp_file_path, meta_path=meta_file_path), error=True) | 177 content_path=content_file_path, meta_path=meta_file_path), error=True) |
151 self.host.quit(1) | 178 self.host.quit(1) |
152 | 179 |
153 if not C.bool(mb_data.get('publish', "true")): | 180 if not C.bool(mb_data.get('publish', "true")): |
154 self.disp(u'Publication blocked by "publish" key in metadata, cancelling edition.\n\n' + | 181 self.disp(u'Publication blocked by "publish" key in metadata, cancelling edition.\n\n' + |
155 "temporary file path:\t{file_path}\nmetadata file path:\t{meta_path}".format( | 182 "temporary file path:\t{content_path}\nmetadata file path:\t{meta_path}".format( |
156 file_path=tmp_file_path, meta_path=meta_file_path), error=True) | 183 content_path=content_file_path, meta_path=meta_file_path), error=True) |
157 self.host.quit(0) | 184 self.host.quit(0) |
158 | 185 |
159 if len(content) == 0: | 186 if len(content) == 0: |
160 self.disp(u"Content is empty, cancelling the blog edition") | 187 self.disp(u"Content is empty, cancelling the blog edition") |
161 | 188 |
172 mb_data['id'] = item_ori_mb_data['id'] | 199 mb_data['id'] = item_ori_mb_data['id'] |
173 | 200 |
174 try: | 201 try: |
175 self.host.bridge.mbSend('', '', mb_data, self.profile) | 202 self.host.bridge.mbSend('', '', mb_data, self.profile) |
176 except Exception as e: | 203 except Exception as e: |
177 self.disp(u"Error while sending your blog, the temporary files have been kept at {file_path} and {meta_path}: {reason}".format( | 204 self.disp(u"Error while sending your blog, the temporary files have been kept at {content_path} and {meta_path}: {reason}".format( |
178 file_path=tmp_file_path, meta_path=meta_file_path, reason=e), error=True) | 205 content_path=content_file_path, meta_path=meta_file_path, reason=e), error=True) |
179 self.host.quit(1) | 206 self.host.quit(1) |
180 | 207 |
181 os.unlink(tmp_file_path) | 208 os.unlink(content_file_path) |
182 os.unlink(meta_file_path) | 209 os.unlink(meta_file_path) |
183 | 210 |
184 def start(self): | 211 def start(self): |
185 # we get current syntax to determine file extension | 212 # we get current syntax to determine file extension |
186 current_syntax = self.host.bridge.getParamA("Syntax", "Composition", "value", self.profile) | 213 current_syntax = self.host.bridge.getParamA("Syntax", "Composition", "value", self.profile) |
189 # if there are user defined extension, we use them | 216 # if there are user defined extension, we use them |
190 SYNTAX_EXT.update(config.getConfig(sat_conf, 'jp', CONF_SYNTAX_EXT, {})) | 217 SYNTAX_EXT.update(config.getConfig(sat_conf, 'jp', CONF_SYNTAX_EXT, {})) |
191 | 218 |
192 # we now create a temporary file | 219 # we now create a temporary file |
193 tmp_suff = '.' + SYNTAX_EXT.get(current_syntax, SYNTAX_EXT['']) | 220 tmp_suff = '.' + SYNTAX_EXT.get(current_syntax, SYNTAX_EXT['']) |
194 tmp_file_obj, tmp_file_path = self.getTmpFile(sat_conf, tmp_suff) | 221 content_file_obj, content_file_path = self.getTmpFile(sat_conf, tmp_suff) |
195 | 222 |
196 item_lower = self.args.item.lower() | 223 item_lower = self.args.item.lower() |
197 if item_lower == 'new': | 224 if item_lower == 'new': |
198 self.disp(u'Editing a new blog item', 2) | 225 self.disp(u'Editing a new blog item', 2) |
199 self.edit(sat_conf, tmp_file_path, tmp_file_obj) | 226 self.edit(sat_conf, content_file_path, content_file_obj) |
200 elif item_lower == 'last': | 227 elif item_lower == 'last': |
201 self.disp(u'Editing last published item', 2) | 228 self.disp(u'Editing last published item', 2) |
202 try: | 229 try: |
203 mb_data = self.host.bridge.mbGet('', '', 1, [], {}, self.profile)[0][0] | 230 mb_data = self.host.bridge.mbGet('', '', 1, [], {}, self.profile)[0][0] |
204 except Exception as e: | 231 except Exception as e: |
206 self.host.quit(1) | 233 self.host.quit(1) |
207 | 234 |
208 content = mb_data['content_xhtml'] | 235 content = mb_data['content_xhtml'] |
209 if current_syntax != 'XHTML': | 236 if current_syntax != 'XHTML': |
210 content = self.host.bridge.syntaxConvert(content, 'XHTML', current_syntax, False, self.profile) | 237 content = self.host.bridge.syntaxConvert(content, 'XHTML', current_syntax, False, self.profile) |
211 tmp_file_obj.write(content.encode('utf-8')) | 238 content_file_obj.write(content.encode('utf-8')) |
212 tmp_file_obj.seek(0) | 239 content_file_obj.seek(0) |
213 self.edit(sat_conf, tmp_file_path, tmp_file_obj, mb_data=mb_data) | 240 self.edit(sat_conf, content_file_path, content_file_obj, mb_data=mb_data) |
214 | 241 |
215 | 242 |
216 class Import(base.CommandAnswering): | 243 class Import(base.CommandAnswering): |
217 def __init__(self, host): | 244 def __init__(self, host): |
218 super(Import, self).__init__(host, 'import', use_progress=True, help=_(u'import an external blog')) | 245 super(Import, self).__init__(host, 'import', use_progress=True, help=_(u'import an external blog')) |