Mercurial > libervia-backend
diff sat_frontends/jp/cmd_blog.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | frontends/src/jp/cmd_blog.py@dcc77f23e370 |
children | 0b6adc2672d9 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat_frontends/jp/cmd_blog.py Mon Apr 02 19:44:50 2018 +0200 @@ -0,0 +1,691 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# jp: a SàT command line tool +# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +import base +from sat.core.i18n import _ +from sat_frontends.jp.constants import Const as C +from sat_frontends.jp import common +from sat.tools.common.ansi import ANSI as A +from sat.tools.common import data_objects +from sat.tools.common import uri +from sat.tools import config +from ConfigParser import NoSectionError, NoOptionError +from functools import partial +import json +import sys +import os.path +import os +import time +import tempfile +import subprocess +import codecs +from sat.tools.common import data_format + +__commands__ = ["Blog"] + +SYNTAX_XHTML = u'xhtml' +# extensions to use with known syntaxes +SYNTAX_EXT = { + '': 'txt', # used when the syntax is not found + SYNTAX_XHTML: "xhtml", + "markdown": "md" + } + + +CONF_SYNTAX_EXT = u'syntax_ext_dict' +BLOG_TMP_DIR = u"blog" +# key to remove from metadata tmp file if they exist +KEY_TO_REMOVE_METADATA = ('id','content', 'content_xhtml', 'comments_node', 'comments_service', 'updated') + +URL_REDIRECT_PREFIX = 'url_redirect_' +INOTIFY_INSTALL = '"pip install inotify"' +MB_KEYS = (u"id", + u"url", + u"atom_id", + u"updated", + u"published", + u"language", + u"comments", # this key is used for all comments* keys + u"tags", # this key is used for all tag* keys + u"author", + u"author_jid", + u"author_email", + u"author_jid_verified", + u"content", + u"content_xhtml", + u"title", + u"title_xhtml", + ) +OUTPUT_OPT_NO_HEADER = u'no-header' + + +def guessSyntaxFromPath(host, sat_conf, path): + """Return syntax guessed according to filename extension + + @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration + @param path(str): path to the content file + @return(unicode): syntax to use + """ + # we first try to guess syntax with extension + ext = os.path.splitext(path)[1][1:] # we get extension without the '.' + if ext: + for k,v in SYNTAX_EXT.iteritems(): + if k and ext == v: + return k + + # if not found, we use current syntax + return host.bridge.getParamA("Syntax", "Composition", "value", host.profile) + + +class BlogPublishCommon(object): + """handle common option for publising commands (Set and Edit)""" + + def add_parser_options(self): + self.parser.add_argument("-T", '--title', type=base.unicode_decoder, help=_(u"title of the item")) + self.parser.add_argument("-t", '--tag', type=base.unicode_decoder, action='append', help=_(u"tag (category) of your item")) + self.parser.add_argument("-C", "--comments", action='store_true', help=_(u"enable comments")) + self.parser.add_argument("-S", '--syntax', type=base.unicode_decoder, help=_(u"syntax to use (default: get profile's default syntax)")) + + def setMbDataContent(self, content, mb_data): + if self.args.syntax is None: + # default syntax has been used + mb_data['content_rich'] = content + elif self.current_syntax == SYNTAX_XHTML: + mb_data['content_xhtml'] = content + else: + mb_data['content_xhtml'] = self.host.bridge.syntaxConvert(content, self.current_syntax, SYNTAX_XHTML, False, self.profile) + + def setMbDataFromArgs(self, mb_data): + """set microblog metadata according to command line options + + if metadata already exist, it will be overwritten + """ + mb_data['allow_comments'] = C.boolConst(self.args.comments) + if self.args.tag: + data_format.iter2dict('tag', self.args.tag, mb_data, check_conflict=False) + if self.args.title is not None: + mb_data['title'] = self.args.title + + +class Set(base.CommandBase, BlogPublishCommon): + + def __init__(self, host): + base.CommandBase.__init__(self, host, 'set', use_pubsub=True, pubsub_flags={C.SINGLE_ITEM}, + help=_(u'publish a new blog item or update an existing one')) + BlogPublishCommon.__init__(self) + self.need_loop=True + + def add_parser_options(self): + BlogPublishCommon.add_parser_options(self) + + def mbSendCb(self): + self.disp(u"Item published") + self.host.quit(C.EXIT_OK) + + def start(self): + self.pubsub_item = self.args.item + mb_data = {} + self.setMbDataFromArgs(mb_data) + content = codecs.getreader('utf-8')(sys.stdin).read() + self.setMbDataContent(content, mb_data) + + self.host.bridge.mbSend( + self.args.service, + self.args.node, + mb_data, + self.profile, + callback=self.exitCb, + errback=partial(self.errback, + msg=_(u"can't send item: {}"), + exit_code=C.EXIT_BRIDGE_ERRBACK)) + + +class Get(base.CommandBase): + TEMPLATE = u"blog/articles.html" + + def __init__(self, host): + extra_outputs = {'default': self.default_output, + 'fancy': self.fancy_output} + base.CommandBase.__init__(self, host, 'get', use_verbose=True, use_pubsub=True, pubsub_flags={C.MULTI_ITEMS}, + use_output=C.OUTPUT_COMPLEX, extra_outputs=extra_outputs, help=_(u'get blog item(s)')) + self.need_loop=True + + def add_parser_options(self): + # TODO: a key(s) argument to select keys to display + self.parser.add_argument("-k", "--key", type=base.unicode_decoder, action='append', dest='keys', + help=_(u"microblog data key(s) to display (default: depend of verbosity)")) + # TODO: add MAM filters + + def template_data_mapping(self, data): + return {u'items': data_objects.BlogItems(data)} + + def format_comments(self, item, keys): + comments_data = data_format.dict2iterdict(u'comments', item, (u'node', u'service'), pop=True) + lines = [] + for data in comments_data: + lines.append(data[u'comments']) + for k in (u'node', u'service'): + if OUTPUT_OPT_NO_HEADER in self.args.output_opts: + header = u'' + else: + header = C.A_HEADER + k + u': ' + A.RESET + lines.append(header + data[k]) + return u'\n'.join(lines) + + def format_tags(self, item, keys): + tags = data_format.dict2iter('tag', item, pop=True) + return u', '.join(tags) + + def format_updated(self, item, keys): + return self.format_time(item['updated']) + + def format_published(self, item, keys): + return self.format_time(item['published']) + + def format_url(self, item, keys): + return uri.buildXMPPUri(u'pubsub', + subtype=u'microblog', + path=self.metadata[u'service'], + node=self.metadata[u'node'], + item=item[u'id']) + + def get_keys(self): + """return keys to display according to verbosity or explicit key request""" + verbosity = self.args.verbose + if self.args.keys: + if not set(MB_KEYS).issuperset(self.args.keys): + self.disp(u"following keys are invalid: {invalid}.\n" + u"Valid keys are: {valid}.".format( + invalid = u', '.join(set(self.args.keys).difference(MB_KEYS)), + valid = u', '.join(sorted(MB_KEYS))), + error=True) + self.host.quit(C.EXIT_BAD_ARG) + return self.args.keys + else: + if verbosity == 0: + return (u'title', u'content') + elif verbosity == 1: + return (u"title", u"tags", u"author", u"author_jid", u"author_email", u"author_jid_verified", u"published", u"updated", u"content") + else: + return MB_KEYS + + def default_output(self, data): + """simple key/value output""" + items, self.metadata = data + keys = self.get_keys() + + # k_cb use format_[key] methods for complex formattings + k_cb = {} + for k in keys: + try: + callback = getattr(self, "format_" + k) + except AttributeError: + pass + else: + k_cb[k] = callback + for idx, item in enumerate(items): + for k in keys: + if k not in item and k not in k_cb: + continue + if OUTPUT_OPT_NO_HEADER in self.args.output_opts: + header = '' + else: + header = u"{k_fmt}{key}:{k_fmt_e} {sep}".format( + k_fmt = C.A_HEADER, + key = k, + k_fmt_e = A.RESET, + sep = u'\n' if 'content' in k else u'') + value = k_cb[k](item, keys) if k in k_cb else item[k] + self.disp(header + value) + # we want a separation line after each item but the last one + if idx < len(items)-1: + print(u'') + + def format_time(self, timestamp): + """return formatted date for timestamp + + @param timestamp(str,int,float): unix timestamp + @return (unicode): formatted date + """ + fmt = u"%d/%m/%Y %H:%M:%S" + return time.strftime(fmt, time.localtime(float(timestamp))) + + def fancy_output(self, data): + """display blog is a nice to read way + + this output doesn't use keys filter + """ + # thanks to http://stackoverflow.com/a/943921 + rows, columns = map(int, os.popen('stty size', 'r').read().split()) + items, metadata = data + verbosity = self.args.verbose + sep = A.color(A.FG_BLUE, columns * u'▬') + if items: + print(u'\n' + sep + '\n') + + for idx, item in enumerate(items): + title = item.get(u'title') + if verbosity > 0: + author = item[u'author'] + published, updated = item[u'published'], item.get('updated') + else: + author = published = updated = None + if verbosity > 1: + tags = list(data_format.dict2iter('tag', item, pop=True)) + else: + tags = None + content = item.get(u'content') + + if title: + print(A.color(A.BOLD, A.FG_CYAN, item[u'title'])) + meta = [] + if author: + meta.append(A.color(A.FG_YELLOW, author)) + if published: + meta.append(A.color(A.FG_YELLOW, u'on ', self.format_time(published))) + if updated != published: + meta.append(A.color(A.FG_YELLOW, u'(updated on ', self.format_time(updated), u')')) + print(u' '.join(meta)) + if tags: + print(A.color(A.FG_MAGENTA, u', '.join(tags))) + if (title or tags) and content: + print("") + if content: + self.disp(content) + + print(u'\n' + sep + '\n') + + + def mbGetCb(self, mb_result): + self.output(mb_result) + self.host.quit(C.EXIT_OK) + + def mbGetEb(self, failure_): + self.disp(u"can't get blog items: {reason}".format( + reason=failure_), error=True) + self.host.quit(C.EXIT_BRIDGE_ERRBACK) + + def start(self): + self.host.bridge.mbGet( + self.args.service, + self.args.node, + self.args.max, + self.args.items, + {}, + self.profile, + callback=self.mbGetCb, + errback=self.mbGetEb) + + +class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit): + + def __init__(self, host): + base.CommandBase.__init__(self, host, 'edit', use_pubsub=True, pubsub_flags={C.SINGLE_ITEM}, + use_draft=True, use_verbose=True, help=_(u'edit an existing or new blog post')) + BlogPublishCommon.__init__(self) + common.BaseEdit.__init__(self, self.host, BLOG_TMP_DIR, use_metadata=True) + + @property + def current_syntax(self): + if self._current_syntax is None: + self._current_syntax = self.host.bridge.getParamA("Syntax", "Composition", "value", self.profile) + return self._current_syntax + + def add_parser_options(self): + BlogPublishCommon.add_parser_options(self) + self.parser.add_argument("-P", "--preview", action="store_true", help=_(u"launch a blog preview in parallel")) + + def buildMetadataFile(self, content_file_path, mb_data=None): + """Build a metadata file using json + + The file is named after content_file_path, with extension replaced by _metadata.json + @param content_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 + # or re-use the existing one if it exists + meta_file_path = os.path.splitext(content_file_path)[0] + common.METADATA_SUFF + if os.path.exists(meta_file_path): + self.disp(u"Metadata file already exists, we re-use it") + try: + with open(meta_file_path, 'rb') as f: + mb_data = json.load(f) + except (OSError, IOError, ValueError) as e: + self.disp(u"Can't read existing metadata file at {path}, aborting: {reason}".format( + path=meta_file_path, reason=e), error=True) + self.host.quit(1) + else: + mb_data = {} if mb_data is None else mb_data.copy() + + # in all cases, we want to remove unwanted keys + for key in KEY_TO_REMOVE_METADATA: + try: + del mb_data[key] + except KeyError: + pass + # and override metadata with command-line arguments + self.setMbDataFromArgs(mb_data) + + # then we create the file and write metadata there, as JSON dict + # XXX: if we port jp one day on Windows, O_BINARY may need to be added here + with os.fdopen(os.open(meta_file_path, os.O_RDWR | os.O_CREAT | os.O_TRUNC,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, content_file_path, content_file_obj, + mb_data=None): + """Edit the file contening the content using editor, and publish it""" + # we first create metadata file + meta_ori, meta_file_path = self.buildMetadataFile(content_file_path, mb_data) + + # do we need a preview ? + if self.args.preview: + self.disp(u"Preview requested, launching it", 1) + # we redirect outputs to /dev/null to avoid console pollution in editor + # if user wants to see messages, (s)he can call "blog preview" directly + DEVNULL = open(os.devnull, 'wb') + subprocess.Popen([sys.argv[0], "blog", "preview", "--inotify", "true", "-p", self.profile, content_file_path], stdout=DEVNULL, stderr=subprocess.STDOUT) + + # we launch editor + self.runEditor("blog_editor_args", content_file_path, content_file_obj, meta_file_path=meta_file_path, meta_ori=meta_ori) + + def publish(self, content, mb_data): + self.setMbDataContent(content, mb_data) + + if self.pubsub_item is not None: + mb_data['id'] = self.pubsub_item + + self.host.bridge.mbSend(self.pubsub_service, self.pubsub_node, mb_data, self.profile) + self.disp(u"Blog item published") + + + def getTmpSuff(self): + # we get current syntax to determine file extension + return SYNTAX_EXT.get(self.current_syntax, SYNTAX_EXT['']) + + def getItemData(self, service, node, item): + items = [item] if item is not None else [] + mb_data = self.host.bridge.mbGet(service, node, 1, items, {}, self.profile)[0][0] + try: + content = mb_data['content_xhtml'] + except KeyError: + content = mb_data['content'] + if content: + content = self.host.bridge.syntaxConvert(content, 'text', SYNTAX_XHTML, False, self.profile) + if content and self.current_syntax != SYNTAX_XHTML: + content = self.host.bridge.syntaxConvert(content, SYNTAX_XHTML, self.current_syntax, False, self.profile) + if content and self.current_syntax == SYNTAX_XHTML: + try: + from lxml import etree + except ImportError: + self.disp(_(u"You need lxml to edit pretty XHTML")) + else: + parser = etree.XMLParser(remove_blank_text=True) + root = etree.fromstring(content, parser) + content = etree.tostring(root, encoding=unicode, pretty_print=True) + + return content, mb_data, mb_data['id'] + + def start(self): + # if there are user defined extension, we use them + SYNTAX_EXT.update(config.getConfig(self.sat_conf, 'jp', CONF_SYNTAX_EXT, {})) + self._current_syntax = self.args.syntax + if self._current_syntax is not None: + try: + self._current_syntax = self.args.syntax = self.host.bridge.syntaxGet(self.current_syntax) + except Exception as e: + if "NotFound" in unicode(e): # FIXME: there is not good way to check bridge errors + self.parser.error(_(u"unknown syntax requested ({syntax})").format(syntax=self.args.syntax)) + else: + raise e + + self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path, content_file_obj, mb_data = self.getItemPath() + + self.edit(content_file_path, content_file_obj, mb_data=mb_data) + + +class Preview(base.CommandBase): + # TODO: need to be rewritten with template output + + def __init__(self, host): + base.CommandBase.__init__(self, host, 'preview', use_verbose=True, help=_(u'preview a blog content')) + + def add_parser_options(self): + self.parser.add_argument("--inotify", type=str, choices=('auto', 'true', 'false'), default=u'auto', help=_(u"use inotify to handle preview")) + self.parser.add_argument("file", type=base.unicode_decoder, nargs='?', default=u'current', help=_(u"path to the content file")) + + def showPreview(self): + # we implement showPreview here so we don't have to import webbrowser and urllib + # when preview is not used + url = 'file:{}'.format(self.urllib.quote(self.preview_file_path)) + self.webbrowser.open_new_tab(url) + + def _launchPreviewExt(self, cmd_line, opt_name): + url = 'file:{}'.format(self.urllib.quote(self.preview_file_path)) + args = common.parse_args(self.host, cmd_line, url=url, preview_file=self.preview_file_path) + if not args: + self.disp(u"Couln't find command in \"{name}\", abording".format(name=opt_name), error=True) + self.host.quit(1) + subprocess.Popen(args) + + def openPreviewExt(self): + self._launchPreviewExt(self.open_cb_cmd, "blog_preview_open_cmd") + + def updatePreviewExt(self): + self._launchPreviewExt(self.update_cb_cmd, "blog_preview_update_cmd") + + def updateContent(self): + with open(self.content_file_path, 'rb') as f: + content = f.read().decode('utf-8-sig') + if content and self.syntax != SYNTAX_XHTML: + # we use safe=True because we want to have a preview as close as possible to what the + # people will see + content = self.host.bridge.syntaxConvert(content, self.syntax, SYNTAX_XHTML, True, self.profile) + + xhtml = (u'<html xmlns="http://www.w3.org/1999/xhtml">' + + u'<head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" /></head>'+ + '<body>{}</body>' + + u'</html>').format(content) + + with open(self.preview_file_path, 'wb') as f: + f.write(xhtml.encode('utf-8')) + + def start(self): + import webbrowser + import urllib + self.webbrowser, self.urllib = webbrowser, urllib + + if self.args.inotify != 'false': + try: + import inotify.adapters + import inotify.constants + from inotify.calls import InotifyError + except ImportError: + if self.args.inotify == 'auto': + inotify = None + self.disp(u'inotify module not found, deactivating feature. You can install it with {install}'.format(install=INOTIFY_INSTALL)) + else: + self.disp(u"inotify not found, can't activate the feature! Please install it with {install}".format(install=INOTIFY_INSTALL), error=True) + self.host.quit(1) + else: + # we deactivate logging in inotify, which is quite annoying + try: + inotify.adapters._LOGGER.setLevel(40) + except AttributeError: + self.disp(u"Logger doesn't exists, inotify may have chanded", error=True) + else: + inotify=None + + sat_conf = config.parseMainConf() + SYNTAX_EXT.update(config.getConfig(sat_conf, 'jp', CONF_SYNTAX_EXT, {})) + + try: + self.open_cb_cmd = config.getConfig(sat_conf, 'jp', "blog_preview_open_cmd", Exception) + except (NoOptionError, NoSectionError): + self.open_cb_cmd = None + open_cb = self.showPreview + else: + open_cb = self.openPreviewExt + + self.update_cb_cmd = config.getConfig(sat_conf, 'jp', "blog_preview_update_cmd", self.open_cb_cmd) + if self.update_cb_cmd is None: + update_cb = self.showPreview + else: + update_cb = self.updatePreviewExt + + # which file do we need to edit? + if self.args.file == 'current': + self.content_file_path = self.getCurrentFile(sat_conf, self.profile) + else: + self.content_file_path = os.path.abspath(self.args.file) + + self.syntax = self.guessSyntaxFromPath(sat_conf, self.content_file_path) + + + # at this point the syntax is converted, we can display the preview + preview_file = tempfile.NamedTemporaryFile(suffix='.xhtml', delete=False) + self.preview_file_path = preview_file.name + preview_file.close() + self.updateContent() + + if inotify is None: + # XXX: we don't delete file automatically because browser need it (and webbrowser.open can return before it is read) + self.disp(u'temporary file created at {}\nthis file will NOT BE DELETED AUTOMATICALLY, please delete it yourself when you have finished'.format(self.preview_file_path)) + open_cb() + else: + open_cb() + i = inotify.adapters.Inotify(block_duration_s=60) # no need for 1 s duraction, inotify drive actions here + + def add_watch(): + i.add_watch(self.content_file_path, mask=inotify.constants.IN_CLOSE_WRITE | + inotify.constants.IN_DELETE_SELF | + inotify.constants.IN_MOVE_SELF) + add_watch() + + try: + for event in i.event_gen(): + if event is not None: + self.disp(u"Content updated", 1) + if {"IN_DELETE_SELF", "IN_MOVE_SELF"}.intersection(event[1]): + self.disp(u"{} event catched, changing the watch".format(", ".join(event[1])), 2) + try: + add_watch() + except InotifyError: + # if the new file is not here yet we can have an error + # as a workaround, we do a little rest + time.sleep(1) + add_watch() + self.updateContent() + update_cb() + except InotifyError: + self.disp(u"Can't catch inotify events, as the file been deleted?", error=True) + finally: + os.unlink(self.preview_file_path) + try: + i.remove_watch(self.content_file_path) + except InotifyError: + pass + + +class Import(base.CommandAnswering): + def __init__(self, host): + super(Import, self).__init__(host, 'import', use_pubsub=True, use_progress=True, help=_(u'import an external blog')) + self.need_loop=True + + def add_parser_options(self): + self.parser.add_argument("importer", type=base.unicode_decoder, nargs='?', help=_(u"importer name, nothing to display importers list")) + self.parser.add_argument('--host', type=base.unicode_decoder, help=_(u"original blog host")) + self.parser.add_argument('--no-images-upload', action='store_true', help=_(u"do *NOT* upload images (default: do upload images)")) + self.parser.add_argument('--upload-ignore-host', help=_(u"do not upload images from this host (default: upload all images)")) + self.parser.add_argument("--ignore-tls-errors", action="store_true", help=_("ignore invalide TLS certificate for uploads")) + self.parser.add_argument('-o', '--option', action='append', nargs=2, default=[], metavar=(u'NAME', u'VALUE'), + help=_(u"importer specific options (see importer description)")) + self.parser.add_argument("location", type=base.unicode_decoder, nargs='?', + help=_(u"importer data location (see importer description), nothing to show importer description")) + + def onProgressStarted(self, metadata): + self.disp(_(u'Blog upload started'),2) + + def onProgressFinished(self, metadata): + self.disp(_(u'Blog uploaded successfully'),2) + redirections = {k[len(URL_REDIRECT_PREFIX):]:v for k,v in metadata.iteritems() + if k.startswith(URL_REDIRECT_PREFIX)} + if redirections: + conf = u'\n'.join([ + u'url_redirections_profile = {}'.format(self.profile), + u"url_redirections_dict = {}".format( + # we need to add ' ' before each new line and to double each '%' for ConfigParser + u'\n '.join(json.dumps(redirections, indent=1, separators=(',',': ')).replace(u'%', u'%%').split(u'\n'))), + ]) + self.disp(_(u'\nTo redirect old URLs to new ones, put the following lines in your sat.conf file, in [libervia] section:\n\n{conf}'.format(conf=conf))) + + def onProgressError(self, error_msg): + self.disp(_(u'Error while uploading blog: {}').format(error_msg),error=True) + + def error(self, failure): + self.disp(_("Error while trying to upload a blog: {reason}").format(reason=failure), error=True) + self.host.quit(1) + + def start(self): + if self.args.location is None: + for name in ('option', 'service', 'no_images_upload'): + if getattr(self.args, name): + self.parser.error(_(u"{name} argument can't be used without location argument").format(name=name)) + if self.args.importer is None: + self.disp(u'\n'.join([u'{}: {}'.format(name, desc) for name, desc in self.host.bridge.blogImportList()])) + else: + try: + short_desc, long_desc = self.host.bridge.blogImportDesc(self.args.importer) + except Exception as e: + msg = [l for l in unicode(e).split('\n') if l][-1] # we only keep the last line + self.disp(msg) + self.host.quit(1) + else: + self.disp(u"{name}: {short_desc}\n\n{long_desc}".format(name=self.args.importer, short_desc=short_desc, long_desc=long_desc)) + self.host.quit() + else: + # we have a location, an import is requested + options = {key: value for key, value in self.args.option} + if self.args.host: + options['host'] = self.args.host + if self.args.ignore_tls_errors: + options['ignore_tls_errors'] = C.BOOL_TRUE + if self.args.no_images_upload: + options['upload_images'] = C.BOOL_FALSE + if self.args.upload_ignore_host: + self.parser.error(u"upload-ignore-host option can't be used when no-images-upload is set") + elif self.args.upload_ignore_host: + options['upload_ignore_host'] = self.args.upload_ignore_host + def gotId(id_): + self.progress_id = id_ + self.host.bridge.blogImport(self.args.importer, self.args.location, options, self.args.service, self.args.node, self.profile, + callback=gotId, errback=self.error) + + +class Blog(base.CommandBase): + subcommands = (Set, Get, Edit, Preview, Import) + + def __init__(self, host): + super(Blog, self).__init__(host, 'blog', use_profile=False, help=_('blog/microblog management'))