Mercurial > libervia-backend
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')) |