Mercurial > libervia-backend
view frontends/src/jp/cmd_blog.py @ 1866:397ef87958b9
jp (blog): edit command, first draft:
- can edit a new or existing item, by default edit a new item
- item are selected with a shortcut, for now only "new" and "last" are handled. URI handling is planed, specially xmpp: URIs
- file are edited using EDITOR env variable, or editor in [jp] section in sat.conf
- current syntax is used, file extension is choosed according to syntax, to make syntax coloration and other goodies in editor available
- if file is empty or not modified, nothing is published
- for now, title, tags and commend desactivation are handled with optional arguments, but other are planed, and a metadata system should come soon
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 01 Mar 2016 01:54:21 +0100 |
parents | 96ba685162f6 |
children | 28b29381db75 |
line wrap: on
line source
#! /usr/bin/python # -*- coding: utf-8 -*- # jp: a SàT command line tool # Copyright (C) 2009-2016 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.core.constants import Const as C from sat.tools import config import json import os.path import os import time import tempfile import subprocess from sat.tools import common __commands__ = ["Blog"] SYNTAX_EXT = { '': 'txt', # used when the syntax is not found "XHTML": "xhtml", "markdown": "md" } CONF_SYNTAX_EXT = 'syntax_ext_dict' BLOG_TMP_DIR="blog" URL_REDIRECT_PREFIX = 'url_redirect_' class Edit(base.CommandBase): def __init__(self, host): super(Edit, self).__init__(host, 'edit', use_verbose=True, help=_(u'edit an existing or new blog post')) def add_parser_options(self): self.parser.add_argument("item", type=base.unicode_decoder, nargs='?', default=u'new', help=_(u"URL of the item to edit, or keyword")) 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("--no-comment", action='store_true', help=_(u"disable comments")) def getTmpFile(self, sat_conf, tmp_suff): 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): try: os.makedirs(tmp_dir) except OSError as e: 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, prefix=time.strftime('blog_%Y-%m-%d_%H:%M:%S_'), dir=tmp_dir, text=True) 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): """Edit the file contening the content using editor, and publish it""" # we first calculate hash to check for modifications import hashlib tmp_file_obj.seek(0) ori_hash = hashlib.sha1(tmp_file_obj.read()).digest() tmp_file_obj.close() # the we launch editor editor = config.getConfig(sat_conf, 'jp', 'editor') or os.getenv('EDITOR', 'vi') editor_exit = subprocess.call([editor, tmp_file]) # 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) else: with open(tmp_file, 'rb') as f: content = f.read() 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(): 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) if self.args.title is not None: mb_data['title'] = self.args.title 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.host.quit(1) os.unlink(tmp_file) def start(self): # we get current syntax to determine file extension current_syntax = self.host.bridge.getParamA("Syntax", "Composition", "value", self.profile) self.disp(u"Current syntax: {}".format(current_syntax), 1) sat_conf = config.parseMainConf() # if there are user defined extension, we use them SYNTAX_EXT.update(config.getConfig(sat_conf, 'jp', CONF_SYNTAX_EXT, {})) # we now create a temporary file tmp_suff = '.' + SYNTAX_EXT.get(current_syntax, SYNTAX_EXT['']) fd, tmp_file = 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)) elif item_lower == 'last': self.disp(u'Editing last published item', 2) try: mb_data = self.host.bridge.mbGet('', '', 1, [], {}, self.profile)[0][0] except Exception as e: self.disp(u"Error while retrieving last comment: {}".format(e)) self.host.quit(1) 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']) class Import(base.CommandAnswering): def __init__(self, host): super(Import, self).__init__(host, 'import', 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('--service', type=base.unicode_decoder, default=u'', metavar=u'PUBSUB_SERVICE', help=_(u"PubSub service where the items must be uploaded (default: server)")) 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: print 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 print msg self.host.quit(1) else: print 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.profile, callback=gotId, errback=self.error) class Blog(base.CommandBase): subcommands = (Edit, Import) def __init__(self, host): super(Blog, self).__init__(host, 'blog', use_profile=False, help=_('blog/microblog management'))