comparison sat_frontends/jp/cmd_blog.py @ 3568:04283582966f

core, frontends: fix invalid translatable strings. Some f-strings where used in translatable text, this has been fixed by using explicit `format()` call (using a script based on `tokenize`). As tokenize messes with spaces, a reformating tool (`black`) has been applied to some files afterwards.
author Goffi <goffi@goffi.org>
date Mon, 14 Jun 2021 18:35:12 +0200
parents 62f490eff51c
children 5f65f4e9f8cb 82e616b70a2a
comparison
equal deleted inserted replaced
3567:a240748ed686 3568:04283582966f
98 if ext: 98 if ext:
99 for k, v in SYNTAX_EXT.items(): 99 for k, v in SYNTAX_EXT.items():
100 if k and ext == v: 100 if k and ext == v:
101 return k 101 return k
102 102
103 # if not found, we use current syntax 103 # if not found, we use current syntax
104 return await host.bridge.getParamA("Syntax", "Composition", "value", host.profile) 104 return await host.bridge.getParamA("Syntax", "Composition", "value", host.profile)
105 105
106 106
107 class BlogPublishCommon(object): 107 class BlogPublishCommon(object):
108 """handle common option for publising commands (Set and Edit)""" 108 """handle common option for publising commands (Set and Edit)"""
123 try: 123 try:
124 syntax = await self.host.bridge.syntaxGet(self.args.syntax) 124 syntax = await self.host.bridge.syntaxGet(self.args.syntax)
125 self.current_syntax = self.args.syntax = syntax 125 self.current_syntax = self.args.syntax = syntax
126 except Exception as e: 126 except Exception as e:
127 if e.classname == "NotFound": 127 if e.classname == "NotFound":
128 self.parser.error(_(f"unknown syntax requested ({self.args.syntax})")) 128 self.parser.error(
129 _("unknown syntax requested ({syntax})").format(
130 syntax=self.args.syntax
131 )
132 )
129 else: 133 else:
130 raise e 134 raise e
131 return self.args.syntax 135 return self.args.syntax
132 136
133 def add_parser_options(self): 137 def add_parser_options(self):
134 self.parser.add_argument( 138 self.parser.add_argument("-T", "--title", help=_("title of the item"))
135 "-T", "--title", help=_("title of the item")
136 )
137 self.parser.add_argument( 139 self.parser.add_argument(
138 "-t", 140 "-t",
139 "--tag", 141 "--tag",
140 action="append", 142 action="append",
141 help=_("tag (category) of your item"), 143 help=_("tag (category) of your item"),
146 help=_("language of the item (ISO 639 code)"), 148 help=_("language of the item (ISO 639 code)"),
147 ) 149 )
148 150
149 comments_group = self.parser.add_mutually_exclusive_group() 151 comments_group = self.parser.add_mutually_exclusive_group()
150 comments_group.add_argument( 152 comments_group.add_argument(
151 "-C", "--comments", action="store_const", const=True, dest="comments", 153 "-C",
152 help=_("enable comments (default: comments not enabled except if they " 154 "--comments",
153 "already exist)") 155 action="store_const",
156 const=True,
157 dest="comments",
158 help=_(
159 "enable comments (default: comments not enabled except if they "
160 "already exist)"
161 ),
154 ) 162 )
155 comments_group.add_argument( 163 comments_group.add_argument(
156 "--no-comments", action="store_const", const=False, dest="comments", 164 "--no-comments",
157 help=_("disable comments (will remove comments node if it exist)") 165 action="store_const",
166 const=False,
167 dest="comments",
168 help=_("disable comments (will remove comments node if it exist)"),
158 ) 169 )
159 170
160 self.parser.add_argument( 171 self.parser.add_argument(
161 "-S", 172 "-S",
162 "--syntax", 173 "--syntax",
180 if metadata already exist, it will be overwritten 191 if metadata already exist, it will be overwritten
181 """ 192 """
182 if self.args.comments is not None: 193 if self.args.comments is not None:
183 mb_data["allow_comments"] = self.args.comments 194 mb_data["allow_comments"] = self.args.comments
184 if self.args.tag: 195 if self.args.tag:
185 mb_data['tags'] = self.args.tag 196 mb_data["tags"] = self.args.tag
186 if self.args.title is not None: 197 if self.args.title is not None:
187 mb_data["title"] = self.args.title 198 mb_data["title"] = self.args.title
188 if self.args.language is not None: 199 if self.args.language is not None:
189 mb_data["language"] = self.args.language 200 mb_data["language"] = self.args.language
190 201
220 self.args.node, 231 self.args.node,
221 data_format.serialise(mb_data), 232 data_format.serialise(mb_data),
222 self.profile, 233 self.profile,
223 ) 234 )
224 except Exception as e: 235 except Exception as e:
225 self.disp( 236 self.disp(f"can't send item: {e}", error=True)
226 f"can't send item: {e}", error=True
227 )
228 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 237 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
229 else: 238 else:
230 self.disp("Item published") 239 self.disp("Item published")
231 self.host.quit(C.EXIT_OK) 240 self.host.quit(C.EXIT_OK)
232 241
259 ) 268 )
260 # TODO: add MAM filters 269 # TODO: add MAM filters
261 270
262 def template_data_mapping(self, data): 271 def template_data_mapping(self, data):
263 items, blog_items = data 272 items, blog_items = data
264 blog_items['items'] = items 273 blog_items["items"] = items
265 return {"blog_items": blog_items} 274 return {"blog_items": blog_items}
266 275
267 def format_comments(self, item, keys): 276 def format_comments(self, item, keys):
268 lines = [] 277 lines = []
269 for data in item.get('comments', []): 278 for data in item.get("comments", []):
270 lines.append(data["uri"]) 279 lines.append(data["uri"])
271 for k in ("node", "service"): 280 for k in ("node", "service"):
272 if OUTPUT_OPT_NO_HEADER in self.args.output_opts: 281 if OUTPUT_OPT_NO_HEADER in self.args.output_opts:
273 header = "" 282 header = ""
274 else: 283 else:
275 header = f"{C.A_HEADER}comments_{k}: {A.RESET}" 284 header = f"{C.A_HEADER}comments_{k}: {A.RESET}"
276 lines.append(header + data[k]) 285 lines.append(header + data[k])
277 return "\n".join(lines) 286 return "\n".join(lines)
278 287
279 def format_tags(self, item, keys): 288 def format_tags(self, item, keys):
280 tags = item.pop('tags', []) 289 tags = item.pop("tags", [])
281 return ", ".join(tags) 290 return ", ".join(tags)
282 291
283 def format_updated(self, item, keys): 292 def format_updated(self, item, keys):
284 return self.format_time(item["updated"]) 293 return self.format_time(item["updated"])
285 294
357 ) 366 )
358 value = k_cb[k](item, keys) if k in k_cb else item[k] 367 value = k_cb[k](item, keys) if k in k_cb else item[k]
359 if isinstance(value, bool): 368 if isinstance(value, bool):
360 value = str(value).lower() 369 value = str(value).lower()
361 self.disp(header + value) 370 self.disp(header + value)
362 # we want a separation line after each item but the last one 371 # we want a separation line after each item but the last one
363 if idx < len(items) - 1: 372 if idx < len(items) - 1:
364 print("") 373 print("")
365 374
366 def format_time(self, timestamp): 375 def format_time(self, timestamp):
367 """return formatted date for timestamp 376 """return formatted date for timestamp
391 author = item["author"] 400 author = item["author"]
392 published, updated = item["published"], item.get("updated") 401 published, updated = item["published"], item.get("updated")
393 else: 402 else:
394 author = published = updated = None 403 author = published = updated = None
395 if verbosity > 1: 404 if verbosity > 1:
396 tags = item.pop('tags', []) 405 tags = item.pop("tags", [])
397 else: 406 else:
398 tags = None 407 tags = None
399 content = item.get("content") 408 content = item.get("content")
400 409
401 if title: 410 if title:
426 self.args.service, 435 self.args.service,
427 self.args.node, 436 self.args.node,
428 self.args.max, 437 self.args.max,
429 self.args.items, 438 self.args.items,
430 self.getPubsubExtra(), 439 self.getPubsubExtra(),
431 self.profile 440 self.profile,
432 ) 441 )
433 ) 442 )
434 except Exception as e: 443 except Exception as e:
435 self.disp(f"can't get blog items: {e}", error=True) 444 self.disp(f"can't get blog items: {e}", error=True)
436 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 445 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
437 else: 446 else:
438 items = mb_data.pop('items') 447 items = mb_data.pop("items")
439 await self.output((items, mb_data)) 448 await self.output((items, mb_data))
440 self.host.quit(C.EXIT_OK) 449 self.host.quit(C.EXIT_OK)
441 450
442 451
443 class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit): 452 class Edit(base.CommandBase, BlogPublishCommon, common.BaseEdit):
481 and path to temporary metadata file 490 and path to temporary metadata file
482 """ 491 """
483 # we first construct metadata from edited item ones and CLI argumments 492 # we first construct metadata from edited item ones and CLI argumments
484 # or re-use the existing one if it exists 493 # or re-use the existing one if it exists
485 meta_file_path = content_file_path.with_name( 494 meta_file_path = content_file_path.with_name(
486 content_file_path.stem + common.METADATA_SUFF) 495 content_file_path.stem + common.METADATA_SUFF
496 )
487 if meta_file_path.exists(): 497 if meta_file_path.exists():
488 self.disp("Metadata file already exists, we re-use it") 498 self.disp("Metadata file already exists, we re-use it")
489 try: 499 try:
490 with meta_file_path.open("rb") as f: 500 with meta_file_path.open("rb") as f:
491 mb_data = json.load(f) 501 mb_data = json.load(f)
497 ) 507 )
498 self.host.quit(1) 508 self.host.quit(1)
499 else: 509 else:
500 mb_data = {} if mb_data is None else mb_data.copy() 510 mb_data = {} if mb_data is None else mb_data.copy()
501 511
502 # in all cases, we want to remove unwanted keys 512 # in all cases, we want to remove unwanted keys
503 for key in KEY_TO_REMOVE_METADATA: 513 for key in KEY_TO_REMOVE_METADATA:
504 try: 514 try:
505 del mb_data[key] 515 del mb_data[key]
506 except KeyError: 516 except KeyError:
507 pass 517 pass
508 # and override metadata with command-line arguments 518 # and override metadata with command-line arguments
509 self.setMbDataFromArgs(mb_data) 519 self.setMbDataFromArgs(mb_data)
510 520
511 if self.args.no_publish: 521 if self.args.no_publish:
512 mb_data["publish"] = False 522 mb_data["publish"] = False
513 523
514 # then we create the file and write metadata there, as JSON dict 524 # then we create the file and write metadata there, as JSON dict
515 # XXX: if we port jp one day on Windows, O_BINARY may need to be added here 525 # XXX: if we port jp one day on Windows, O_BINARY may need to be added here
516 with os.fdopen( 526 with os.fdopen(
517 os.open(meta_file_path, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), "w+b" 527 os.open(meta_file_path, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), "w+b"
518 ) as f: 528 ) as f:
519 # we need to use an intermediate unicode buffer to write to the file 529 # we need to use an intermediate unicode buffer to write to the file
520 # unicode without escaping characters 530 # unicode without escaping characters
554 stdout=DEVNULL, 564 stdout=DEVNULL,
555 stderr=DEVNULL, 565 stderr=DEVNULL,
556 ) 566 )
557 ) 567 )
558 568
559 # we launch editor 569 # we launch editor
560 coroutines.append( 570 coroutines.append(
561 self.runEditor( 571 self.runEditor(
562 "blog_editor_args", 572 "blog_editor_args",
563 content_file_path, 573 content_file_path,
564 content_file_obj, 574 content_file_obj,
588 598
589 async def getItemData(self, service, node, item): 599 async def getItemData(self, service, node, item):
590 items = [item] if item else [] 600 items = [item] if item else []
591 601
592 mb_data = data_format.deserialise( 602 mb_data = data_format.deserialise(
593 await self.host.bridge.mbGet( 603 await self.host.bridge.mbGet(service, node, 1, items, {}, self.profile)
594 service, node, 1, items, {}, self.profile 604 )
595 ) 605 item = mb_data["items"][0]
596 )
597 item = mb_data['items'][0]
598 606
599 try: 607 try:
600 content = item["content_xhtml"] 608 content = item["content_xhtml"]
601 except KeyError: 609 except KeyError:
602 content = item["content"] 610 content = item["content"]
610 content, SYNTAX_XHTML, self.current_syntax, False, self.profile 618 content, SYNTAX_XHTML, self.current_syntax, False, self.profile
611 ) 619 )
612 620
613 if content and self.current_syntax == SYNTAX_XHTML: 621 if content and self.current_syntax == SYNTAX_XHTML:
614 content = content.strip() 622 content = content.strip()
615 if not content.startswith('<div>'): 623 if not content.startswith("<div>"):
616 content = '<div>' + content + '</div>' 624 content = "<div>" + content + "</div>"
617 try: 625 try:
618 from lxml import etree 626 from lxml import etree
619 except ImportError: 627 except ImportError:
620 self.disp(_("You need lxml to edit pretty XHTML")) 628 self.disp(_("You need lxml to edit pretty XHTML"))
621 else: 629 else:
628 async def start(self): 636 async def start(self):
629 # if there are user defined extension, we use them 637 # if there are user defined extension, we use them
630 SYNTAX_EXT.update(config.getConfig(self.sat_conf, "jp", CONF_SYNTAX_EXT, {})) 638 SYNTAX_EXT.update(config.getConfig(self.sat_conf, "jp", CONF_SYNTAX_EXT, {}))
631 self.current_syntax = await self.get_current_syntax() 639 self.current_syntax = await self.get_current_syntax()
632 640
633 (self.pubsub_service, self.pubsub_node, self.pubsub_item, content_file_path, 641 (
634 content_file_obj, mb_data,) = await self.getItemPath() 642 self.pubsub_service,
643 self.pubsub_node,
644 self.pubsub_item,
645 content_file_path,
646 content_file_obj,
647 mb_data,
648 ) = await self.getItemPath()
635 649
636 await self.edit(content_file_path, content_file_obj, mb_data=mb_data) 650 await self.edit(content_file_path, content_file_obj, mb_data=mb_data)
637 self.host.quit() 651 self.host.quit()
638 652
639 653
640 class Rename(base.CommandBase): 654 class Rename(base.CommandBase):
641
642 def __init__(self, host): 655 def __init__(self, host):
643 base.CommandBase.__init__( 656 base.CommandBase.__init__(
644 self, 657 self,
645 host, 658 host,
646 "rename", 659 "rename",
648 pubsub_flags={C.SINGLE_ITEM}, 661 pubsub_flags={C.SINGLE_ITEM},
649 help=_("rename an blog item"), 662 help=_("rename an blog item"),
650 ) 663 )
651 664
652 def add_parser_options(self): 665 def add_parser_options(self):
653 self.parser.add_argument( 666 self.parser.add_argument("new_id", help=_("new item id to use"))
654 "new_id",
655 help=_("new item id to use")
656 )
657 667
658 async def start(self): 668 async def start(self):
659 try: 669 try:
660 await self.host.bridge.mbRename( 670 await self.host.bridge.mbRename(
661 self.args.service, 671 self.args.service,
663 self.args.item, 673 self.args.item,
664 self.args.new_id, 674 self.args.new_id,
665 self.profile, 675 self.profile,
666 ) 676 )
667 except Exception as e: 677 except Exception as e:
668 self.disp( 678 self.disp(f"can't rename item: {e}", error=True)
669 f"can't rename item: {e}", error=True
670 )
671 self.host.quit(C.EXIT_BRIDGE_ERRBACK) 679 self.host.quit(C.EXIT_BRIDGE_ERRBACK)
672 else: 680 else:
673 self.disp("Item renamed") 681 self.disp("Item renamed")
674 self.host.quit(C.EXIT_OK) 682 self.host.quit(C.EXIT_OK)
683
675 684
676 class Preview(base.CommandBase, common.BaseEdit): 685 class Preview(base.CommandBase, common.BaseEdit):
677 # TODO: need to be rewritten with template output 686 # TODO: need to be rewritten with template output
678 687
679 def __init__(self, host): 688 def __init__(self, host):
733 ) 742 )
734 743
735 xhtml = ( 744 xhtml = (
736 f'<html xmlns="http://www.w3.org/1999/xhtml">' 745 f'<html xmlns="http://www.w3.org/1999/xhtml">'
737 f'<head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" />' 746 f'<head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" />'
738 f'</head>' 747 f"</head>"
739 f'<body>{content}</body>' 748 f"<body>{content}</body>"
740 f'</html>' 749 f"</html>"
741 ) 750 )
742 751
743 with open(self.preview_file_path, "wb") as f: 752 with open(self.preview_file_path, "wb") as f:
744 f.write(xhtml.encode("utf-8")) 753 f.write(xhtml.encode("utf-8"))
745 754
789 if self.update_cb_cmd is None: 798 if self.update_cb_cmd is None:
790 update_cb = self.showPreview 799 update_cb = self.showPreview
791 else: 800 else:
792 update_cb = self.updatePreviewExt 801 update_cb = self.updatePreviewExt
793 802
794 # which file do we need to edit? 803 # which file do we need to edit?
795 if self.args.file == "current": 804 if self.args.file == "current":
796 self.content_file_path = self.getCurrentFile(self.profile) 805 self.content_file_path = self.getCurrentFile(self.profile)
797 else: 806 else:
798 try: 807 try:
799 self.content_file_path = Path(self.args.file).resolve(strict=True) 808 self.content_file_path = Path(self.args.file).resolve(strict=True)
800 except FileNotFoundError: 809 except FileNotFoundError:
801 self.disp(_(f'File "{self.args.file}" doesn\'t exist!')) 810 self.disp(_('File "{file}" doesn\'t exist!').format(file=self.args.file))
802 self.host.quit(C.EXIT_NOT_FOUND) 811 self.host.quit(C.EXIT_NOT_FOUND)
803 812
804 self.syntax = await guessSyntaxFromPath( 813 self.syntax = await guessSyntaxFromPath(
805 self.host, sat_conf, self.content_file_path) 814 self.host, sat_conf, self.content_file_path
815 )
806 816
807 # at this point the syntax is converted, we can display the preview 817 # at this point the syntax is converted, we can display the preview
808 preview_file = tempfile.NamedTemporaryFile(suffix=".xhtml", delete=False) 818 preview_file = tempfile.NamedTemporaryFile(suffix=".xhtml", delete=False)
809 self.preview_file_path = preview_file.name 819 self.preview_file_path = preview_file.name
810 preview_file.close() 820 preview_file.close()
823 await open_cb() 833 await open_cb()
824 watcher = aionotify.Watcher() 834 watcher = aionotify.Watcher()
825 watcher_kwargs = { 835 watcher_kwargs = {
826 # Watcher don't accept Path so we convert to string 836 # Watcher don't accept Path so we convert to string
827 "path": str(self.content_file_path), 837 "path": str(self.content_file_path),
828 "alias": 'content_file', 838 "alias": "content_file",
829 "flags": aionotify.Flags.CLOSE_WRITE 839 "flags": aionotify.Flags.CLOSE_WRITE
830 | aionotify.Flags.DELETE_SELF 840 | aionotify.Flags.DELETE_SELF
831 | aionotify.Flags.MOVE_SELF, 841 | aionotify.Flags.MOVE_SELF,
832 } 842 }
833 watcher.watch(**watcher_kwargs) 843 watcher.watch(**watcher_kwargs)
834 844
835 loop = asyncio.get_event_loop() 845 loop = asyncio.get_event_loop()
836 await watcher.setup(loop) 846 await watcher.setup(loop)
837 847
838 try: 848 try:
839 while True: 849 while True:
840 event = await watcher.get_event() 850 event = await watcher.get_event()
841 self.disp("Content updated", 1) 851 self.disp("Content updated", 1)
842 if event.flags & (aionotify.Flags.DELETE_SELF 852 if event.flags & (
843 | aionotify.Flags.MOVE_SELF): 853 aionotify.Flags.DELETE_SELF | aionotify.Flags.MOVE_SELF
854 ):
844 self.disp( 855 self.disp(
845 "DELETE/MOVE event catched, changing the watch", 856 "DELETE/MOVE event catched, changing the watch",
846 2, 857 2,
847 ) 858 )
848 try: 859 try:
849 watcher.unwatch('content_file') 860 watcher.unwatch("content_file")
850 except IOError as e: 861 except IOError as e:
851 self.disp( 862 self.disp(
852 f"Can't remove the watch: {e}", 863 f"Can't remove the watch: {e}",
853 2, 864 2,
854 ) 865 )
867 self.disp("The file seems to have been deleted.", error=True) 878 self.disp("The file seems to have been deleted.", error=True)
868 self.host.quit(C.EXIT_NOT_FOUND) 879 self.host.quit(C.EXIT_NOT_FOUND)
869 finally: 880 finally:
870 os.unlink(self.preview_file_path) 881 os.unlink(self.preview_file_path)
871 try: 882 try:
872 watcher.unwatch('content_file') 883 watcher.unwatch("content_file")
873 except IOError as e: 884 except IOError as e:
874 self.disp( 885 self.disp(
875 f"Can't remove the watch: {e}", 886 f"Can't remove the watch: {e}",
876 2, 887 2,
877 ) 888 )
891 self.parser.add_argument( 902 self.parser.add_argument(
892 "importer", 903 "importer",
893 nargs="?", 904 nargs="?",
894 help=_("importer name, nothing to display importers list"), 905 help=_("importer name, nothing to display importers list"),
895 ) 906 )
896 self.parser.add_argument( 907 self.parser.add_argument("--host", help=_("original blog host"))
897 "--host", help=_("original blog host")
898 )
899 self.parser.add_argument( 908 self.parser.add_argument(
900 "--no-images-upload", 909 "--no-images-upload",
901 action="store_true", 910 action="store_true",
902 help=_("do *NOT* upload images (default: do upload images)"), 911 help=_("do *NOT* upload images (default: do upload images)"),
903 ) 912 )
952 ), 961 ),
953 ] 962 ]
954 ) 963 )
955 self.disp( 964 self.disp(
956 _( 965 _(
957 f"\nTo redirect old URLs to new ones, put the following lines in your" 966 "\nTo redirect old URLs to new ones, put the following lines in your"
958 f" sat.conf file, in [libervia] section:\n\n{conf}" 967 " sat.conf file, in [libervia] section:\n\n{conf}"
959 ) 968 ).format(conf=conf)
960 ) 969 )
961 970
962 async def onProgressError(self, error_msg): 971 async def onProgressError(self, error_msg):
963 self.disp(_(f"Error while uploading blog: {error_msg}"), error=True) 972 self.disp(
973 _("Error while uploading blog: {error_msg}").format(error_msg=error_msg),
974 error=True,
975 )
964 976
965 async def start(self): 977 async def start(self):
966 if self.args.location is None: 978 if self.args.location is None:
967 for name in ("option", "service", "no_images_upload"): 979 for name in ("option", "service", "no_images_upload"):
968 if getattr(self.args, name): 980 if getattr(self.args, name):
969 self.parser.error( 981 self.parser.error(
970 _( 982 _(
971 f"{name} argument can't be used without location argument" 983 "{name} argument can't be used without location argument"
972 ) 984 ).format(name=name)
973 ) 985 )
974 if self.args.importer is None: 986 if self.args.importer is None:
975 self.disp( 987 self.disp(
976 "\n".join( 988 "\n".join(
977 [ 989 [
1020 self.args.node, 1032 self.args.node,
1021 self.profile, 1033 self.profile,
1022 ) 1034 )
1023 except Exception as e: 1035 except Exception as e:
1024 self.disp( 1036 self.disp(
1025 _(f"Error while trying to import a blog: {e}"), 1037 _("Error while trying to import a blog: {e}").format(e=e),
1026 error=True, 1038 error=True,
1027 ) 1039 )
1028 self.host.quit(1) 1040 self.host.quit(1)
1029 1041
1030 await self.set_progress_id(progress_id) 1042 await self.set_progress_id(progress_id)
1031 1043
1044
1032 class Blog(base.CommandBase): 1045 class Blog(base.CommandBase):
1033 subcommands = (Set, Get, Edit, Rename, Preview, Import) 1046 subcommands = (Set, Get, Edit, Rename, Preview, Import)
1034 1047
1035 def __init__(self, host): 1048 def __init__(self, host):
1036 super(Blog, self).__init__( 1049 super(Blog, self).__init__(