comparison frontends/src/jp/common.py @ 2532:772447ec070f

jp: pubsub options refactoring: There is now only "use_pubsub", and specification are set using "pubsub_flags" argument when instantiating CommandBase. Options are more Python Zen compliant by using explicit arguments for item, draft, url instead of trying to guess with magic keyword and type detection. Pubsub node and item are now always using respecively "-n" and "-i" even when required, this way shell history can be used to change command more easily, and it's globally less confusing for user. if --pubsub-url is used, elements can be overwritten with individual option (e.g. change item id with --item). New "use_draft" argument in CommandBase, to re-use current draft or open a file path as draft. Item can now be specified when using a draft. If it already exists, its content will be added to current draft (with a separator), to avoid loosing data. common.BaseEdit.getItemPath could be simplified thanks to those changes. Pubsub URI handling has been moved to base.py.
author Goffi <goffi@goffi.org>
date Wed, 21 Mar 2018 19:13:22 +0100
parents 21d43eab3fb9
children b27165bf160c
comparison
equal deleted inserted replaced
2531:1dfc5516dead 2532:772447ec070f
19 19
20 from sat_frontends.jp.constants import Const as C 20 from sat_frontends.jp.constants import Const as C
21 from sat.core.i18n import _ 21 from sat.core.i18n import _
22 from sat.core import exceptions 22 from sat.core import exceptions
23 from sat.tools.common import regex 23 from sat.tools.common import regex
24 from sat.tools.common import uri
25 from sat.tools.common.ansi import ANSI as A 24 from sat.tools.common.ansi import ANSI as A
26 from sat.tools import config 25 from sat.tools import config
27 from ConfigParser import NoSectionError, NoOptionError 26 from ConfigParser import NoSectionError, NoOptionError
28 from collections import namedtuple 27 from collections import namedtuple
29 import json 28 import json
103 except ValueError as e: 102 except ValueError as e:
104 host.disp(u"Couldn't parse editor cmd [{cmd}]: {reason}".format(cmd=cmd_line, reason=e)) 103 host.disp(u"Couldn't parse editor cmd [{cmd}]: {reason}".format(cmd=cmd_line, reason=e))
105 return [] 104 return []
106 105
107 106
108 def checkURI(args):
109 """check if args.node is an URI
110
111 if a valid xmpp: URI is found, args.service, args.node and args.item will be set
112 """
113 # FIXME: Q&D way to handle xmpp: uris, a generic way is needed
114 # and it should be merged with code in BaseEdit
115 if not args.service and args.node.startswith('xmpp:'):
116 try:
117 uri_data = uri.parseXMPPUri(args.node)
118 except ValueError:
119 pass
120 else:
121 if uri_data[u'type'] == 'pubsub':
122 args.service = uri_data[u'path']
123 args.node = uri_data[u'node']
124 if u'item' in uri_data:
125 try:
126 item = getattr(uri_data, 'item')
127 except AttributeError:
128 pass
129 else:
130 if item is None:
131 args.item = uri_data
132
133
134 class BaseEdit(object): 107 class BaseEdit(object):
135 u"""base class for editing commands 108 u"""base class for editing commands
136 109
137 This class allows to edit file for PubSub or something else. 110 This class allows to edit file for PubSub or something else.
138 It works with temporary files in SàT local_dir, in a "cat_dir" subdir 111 It works with temporary files in SàT local_dir, in a "cat_dir" subdir
139 """ 112 """
140 # use_items(bool): True if items are used, will then add item related options
141 use_items=True
142 113
143 def __init__(self, host, cat_dir, use_metadata=False): 114 def __init__(self, host, cat_dir, use_metadata=False):
144 """ 115 """
145 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration 116 @param sat_conf(ConfigParser.ConfigParser): instance opened on sat configuration
146 @param cat_dir(unicode): directory to use for drafts 117 @param cat_dir(unicode): directory to use for drafts
151 """ 122 """
152 self.host = host 123 self.host = host
153 self.sat_conf = config.parseMainConf() 124 self.sat_conf = config.parseMainConf()
154 self.cat_dir_str = cat_dir.encode('utf-8') 125 self.cat_dir_str = cat_dir.encode('utf-8')
155 self.use_metadata = use_metadata 126 self.use_metadata = use_metadata
156
157 def add_parser_options(self):
158 if self.use_items:
159 group = self.parser.add_mutually_exclusive_group()
160 group.add_argument("--force-item", action='store_true', help=_(u"don't use magic and take item argument as an actual item"))
161 group.add_argument("--last-item", action='store_true', help=_(u"take last item instead of creating a new one if no item id is found"))
162 127
163 def secureUnlink(self, path): 128 def secureUnlink(self, path):
164 """Unlink given path after keeping it for a while 129 """Unlink given path after keeping it for a while
165 130
166 This method is used to prevent accidental deletion of a draft 131 This method is used to prevent accidental deletion of a draft
363 328
364 def getTmpSuff(self): 329 def getTmpSuff(self):
365 """return suffix used for content file""" 330 """return suffix used for content file"""
366 return u'xml' 331 return u'xml'
367 332
368 def getItemPath(self, item): 333 def getItemPath(self):
369 """retrieve item path (i.e. service and node) from item argument 334 """retrieve item path (i.e. service and node) from item argument
370 335
371 This method is obviously only useful for edition of PubSub based features 336 This method is obviously only useful for edition of PubSub based features
372 service, node and item must be named like this in args 337 """
373 @param item(unicode): item to get or url or magic keyword 338 service = self.args.service
374 item argument can be used to specify : 339 node = self.args.node
375 - HTTP(S) URL 340 item = self.args.item
376 - XMPP URL 341 last_item = self.args.last_item
377 - keyword, which can be: 342
378 - new: create new item 343 if self.args.current:
379 - last: retrieve last published item 344 # user wants to continue current draft
380 - current: continue current local draft 345 content_file_path = self.getCurrentFile(self.profile)
381 - file path 346 self.disp(u'Continuing edition of current draft', 2)
382 - item id 347 content_file_obj = open(content_file_path, 'r+b')
383 """ 348 # we seek at the end of file in case of an item already exist
384 force_item = self.args.force_item 349 # this will write content of the existing item at the end of the draft.
385 if force_item and not item: 350 # This way no data should be lost.
386 self.parser.error(_(u"an item id must be specified if you use --force-item")) 351 content_file_obj.seek(0, os.SEEK_END)
387 command = item.lower() 352 elif self.args.draft_path:
388 pubsub_service = self.args.service 353 # there is an existing draft that we use
389 pubsub_node = self.args.node 354 content_file_path = os.path.expanduser(self.args.item)
390 pubsub_item = None 355 content_file_obj = open(content_file_path, 'r+b')
391 356 # we seek at the end for the same reason as above
392 if not force_item and command not in ('new', 'last', 'current'): 357 content_file_obj.seek(0, os.SEEK_END)
393 # we have probably an URL, we try to parse it 358 else:
394 import urlparse
395 url = self.args.item
396 parsed_url = urlparse.urlsplit(url.encode('utf-8'))
397 if parsed_url.scheme.startswith('http'):
398 self.disp(u"{} URL found, trying to find associated xmpp: URI".format(parsed_url.scheme.upper()),1)
399 # HTTP URL, we try to find xmpp: links
400 try:
401 from lxml import etree
402 except ImportError:
403 self.disp(u"lxml module must be installed to use http(s) scheme, please install it with \"pip install lxml\"", error=True)
404 self.host.quit(1)
405 import urllib2
406 parser = etree.HTMLParser()
407 try:
408 root = etree.parse(urllib2.urlopen(url), parser)
409 except etree.XMLSyntaxError as e:
410 self.disp(_(u"Can't parse HTML page : {msg}").format(msg=e))
411 links = []
412 else:
413 links = root.xpath("//link[@rel='alternate' and starts-with(@href, 'xmpp:')]")
414 if not links:
415 self.disp(u'Could not find alternate "xmpp:" URI, can\'t find associated XMPP PubSub node/item', error=True)
416 self.host.quit(1)
417 url = links[0].get('href')
418 parsed_url = urlparse.urlsplit(url)
419
420 if parsed_url.scheme == 'xmpp':
421 if self.args.service or self.args.node:
422 self.parser.error(_(u"You can't use URI and --service or --node at the same time"))
423
424 self.disp(u"XMPP URI used: {}".format(url),2)
425 # XXX: if we have not xmpp: URI here, we'll take the data as a file path
426 pubsub_service = parsed_url.path.decode('utf-8')
427 pubsub_data = urlparse.parse_qs(parsed_url.query)
428 try:
429 pubsub_node = pubsub_data['node'][0].decode('utf-8')
430 except KeyError:
431 self.disp(u'No node found in xmpp: URI, can\'t retrieve item', error=True)
432 self.host.quit(1)
433 pubsub_item = pubsub_data.get('item',[None])[0]
434 if pubsub_item is not None:
435 pubsub_item = pubsub_item.decode('utf-8')
436 if pubsub_item is None and self.args.last_item:
437 command = 'last'
438 elif pubsub_item is not None:
439 command = 'edit' # XXX: edit command is only used internaly, it similar to last, but with the item given in the URL
440 else:
441 command = 'new'
442
443 if self.args.last_item:
444 if pubsub_item is None:
445 command = 'last'
446 elif command != 'last':
447 self.parser.error(_(u"--last-item can't be used with a specified item"))
448
449 if not force_item and command in ('new', 'last', 'edit'):
450 # we need a temporary file 359 # we need a temporary file
451 content_file_obj, content_file_path = self.getTmpFile() 360 content_file_obj, content_file_path = self.getTmpFile()
452 if command == 'new': 361
453 self.disp(u'Editing a new item', 2) 362 if item or last_item:
363 self.disp(u'Editing requested published item', 2)
364 try:
454 if self.use_metadata: 365 if self.use_metadata:
366 content, metadata, item = self.getItemData(service, node, item)
367 else:
368 content, item = self.getItemData(service, node, item)
369 except Exception as e:
370 # FIXME: ugly but we have not good may to check errors in bridge
371 if u'item-not-found' in unicode(e):
372 # item doesn't exist, we create a new one with requested id
455 metadata = None 373 metadata = None
456 elif command in ('last', 'edit'): 374 if last_item:
457 self.disp(u'Editing requested published item', 2) 375 self.disp(_(u'no item found at all, we create a new one'), 2)
458 try:
459 if self.use_metadata:
460 content, metadata, pubsub_item = self.getItemData(pubsub_service, pubsub_node, pubsub_item)
461 else: 376 else:
462 content, pubsub_item = self.getItemData(pubsub_service, pubsub_node, pubsub_item) 377 self.disp(_(u'item "{item_id}" not found, we create a new item with this id').format(item_id=item), 2)
463 except Exception as e: 378 content_file_obj.seek(0)
464 self.disp(u"Error while retrieving last item: {}".format(e)) 379 else:
465 self.host.quit(1) 380 self.disp(u"Error while retrieving item: {}".format(e))
381 self.host.quit(C.EXIT_ERROR)
382 else:
383 # item exists, we write content
384 if content_file_obj.tell() != 0:
385 # we already have a draft,
386 # we copy item content after it and add an indicator
387 content_file_obj.write('\n*****\n')
466 content_file_obj.write(content.encode('utf-8')) 388 content_file_obj.write(content.encode('utf-8'))
467 content_file_obj.seek(0) 389 content_file_obj.seek(0)
468 else: 390 self.disp(_(u'item "{item_id}" found, we edit it').format(item_id=item), 2)
391 else:
392 self.disp(u'Editing a new item', 2)
469 if self.use_metadata: 393 if self.use_metadata:
470 metadata = None 394 metadata = None
471 if not force_item and command == 'current':
472 # user wants to continue current draft
473 content_file_path = self.getCurrentFile(self.profile)
474 self.disp(u'Continuing edition of current draft', 2)
475 content_file_obj = open(content_file_path, 'r+b')
476 elif not force_item and os.path.isfile(self.args.item):
477 # there is an existing draft that we use
478 content_file_path = os.path.expanduser(self.args.item)
479 content_file_obj = open(content_file_path, 'r+b')
480 else:
481 # last chance, it should be an item
482 content_file_obj, content_file_path = self.getTmpFile()
483 pubsub_item = self.args.item
484
485 try:
486 # we try to get existing item
487 if self.use_metadata:
488 content, metadata, pubsub_item = self.getItemData(pubsub_service, pubsub_node, self.args.item)
489 else:
490 content, pubsub_item = self.getItemData(pubsub_service, pubsub_node, self.args.item)
491 except Exception as e:
492 # FIXME: ugly but we have not good may to check errors in bridge
493 if u'item-not-found' in unicode(e):
494 # item doesn't exist, we create a new one with requested id
495 metadata = None
496 self.disp(_(u'item "{item_id}" not found, we create a new item with this id').format(item_id=pubsub_item), 2)
497 else:
498 # item exists, we write content if content file
499 content_file_obj.write(content.encode('utf-8'))
500 content_file_obj.seek(0)
501 self.disp(_(u'item "{item_id}" found, we edit it').format(item_id=pubsub_item), 2)
502 395
503 if self.use_metadata: 396 if self.use_metadata:
504 return pubsub_service, pubsub_node, pubsub_item, content_file_path, content_file_obj, metadata 397 return service, node, item, content_file_path, content_file_obj, metadata
505 else: 398 else:
506 return pubsub_service, pubsub_node, pubsub_item, content_file_path, content_file_obj 399 return service, node, item, content_file_path, content_file_obj
507 400
508 401
509 class Table(object): 402 class Table(object):
510 403
511 def __init__(self, host, data, headers=None, filters=None, use_buffer=False): 404 def __init__(self, host, data, headers=None, filters=None, use_buffer=False):