#!/usr/bin/env python # -*- coding: utf-8 -*- # # markpages.py # # Copyright 2017 Jonathan Golder # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # # """ Bot to mark pages which were/are subjects of redundance discussions with templates """ import re from datetime import datetime import pywikibot from pywikibot import pagegenerators from pywikibot.bot import CurrentPageBot from pywikibot.diff import PatchManager import mwparserfromhell as mwparser import jogobot from lib.redfam import RedFamWorker class MarkPagesBot( CurrentPageBot ): # sets 'current_page' on each treat() """ Bot class to mark pages which were/are subjects of redundance discussions with templates """ def __init__( self, genFactory, **kwargs ): """ Constructor Parameters: @param genFactory GenFactory with parsed pagegenerator args to build generator @type genFactory pagegenerators.GeneratorFactory @param **kwargs Additional args @type iterable """ # Init attribute self.__redfams = None # Will hold a generator with our redfams if "famhash" in kwargs: self.famhash = kwargs["famhash"] # We do not use predefined genFactory as there is no sensefull case to # give a generator via cmd-line for this right now self.genFactory = pagegenerators.GeneratorFactory() # Build generator with genFactory self.build_generator() # Run super class init with builded generator super( MarkPagesBot, self ).__init__( generator=self.gen, always=True if "always" in kwargs else False ) def run(self): """ Controls the overal parsing process, using super class for page switch Needed to do things before/after treating pages is done """ try: super( MarkPagesBot, self ).run() except: raise else: # Do status redfam status updates for redfam in self.redfams: redfam.update_status() RedFamWorker.flush_db_cache() @property def redfams(self): """ Holds redfams generator to work on in this bot """ # Create generator if not present if not self.__redfams: end_after = datetime.strptime( jogobot.config["red.markpages"]["mark_done_after"], "%Y-%m-%d" ) if hasattr(self, "famhash"): self.__redfams = list( RedFamWorker.session.query(RedFamWorker).filter( RedFamWorker.famhash == self.famhash ) ) else: self.__redfams = list( RedFamWorker.gen_by_status_and_ending( "archived", end_after) ) return self.__redfams def build_generator( self ): """ Builds generator to pass to super class """ # Add Talkpages to work on to generatorFactory self.genFactory.gens.append( self.redfam_talkpages_generator() ) # Set generator to pass to super class # Since PreloadingGenerator mixis up the Pages, do not use it right now # (FS#148) # We can do so for automatic runs (FS#150) # self.gen = pagegenerators.PreloadingGenerator( # self.genFactory.getCombinedGenerator() ) self.gen = self.genFactory.getCombinedGenerator() def redfam_talkpages_generator( self ): """ Wrappers the redfam.article_generator and passes it to pagegenerators.PageWithTalkPageGenerator(). Then it iterates over the generator and adds a reference to the related redfam to each talkpage-object. """ for redfam in self.redfams: # We need the talkpage (and only this) of each existing page for talkpage in redfam.article_generator( filter_existing=True, exclude_article_status=["marked"], talkpages=True ): yield talkpage def treat_page( self ): """ Handles work on current page We get a reference to related redfam in current_page.redfam """ # First we need to have the current text of page # and parse it as wikicode self.current_wikicode = mwparser.parse( self.current_page.text ) # Add notice # Returns True if added # None if already present add_ret = self.add_disc_notice_template() # Convert wikicode back to string to save self.new_text = str( self.current_wikicode ) # Define edit summary summary = jogobot.config["red.markpages"]["mark_done_summary"].format( reddisc=self.current_page.redfam.get_disc_link() ).strip() # Make sure summary starts with "Bot:" if not summary[:len("Bot:")] == "Bot:": summary = "Bot: " + summary.strip() # will return True if saved # False if not saved because of errors # None if change was not accepted by user save_ret = self.put_current( self.new_text, summary=summary ) # Get article as named in db article = self.current_page.redarticle # Status if add_ret is None or ( add_ret and save_ret ): self.current_page.redfam.article_remove_status( "note_rej", title=article) self.current_page.redfam.article_remove_status( "sav_err", title=article) self.current_page.redfam.article_add_status( "marked", title=article) elif save_ret is None: self.current_page.redfam.article_add_status( "note_rej", title=article) else: self.current_page.redfam.article_add_status( "sav_err", title=article) def add_disc_notice_template( self ): """ Will take self.current_wikicode and adds disc notice template after the last template in leading section or as first element if there is no other template in leading section """ # The notice to add self.disc_notice = \ self.current_page.redfam.generate_disc_notice_template() # Check if it is already present in wikicode if self.disc_notice_present(): return # Find the right place to insert notice template # Therfore we need the first section (if there is one) leadsec = self.current_wikicode.get_sections( flat=False, include_lead=True )[0] # There is none on empty pages, so we need to check if leadsec: # Get the last template in leadsec ltemplates = leadsec.filter_templates(recursive=False) # If there is one, add notice after this if ltemplates: # Make sure not separate template and maybe following comment insert_after_index = self.current_wikicode.index( ltemplates[-1] ) # If there is more content if len(self.current_wikicode.nodes) > (insert_after_index + 1): # Filter one linebreak if isinstance( self.current_wikicode.get( insert_after_index + 1), mwparser.nodes.text.Text) and \ re.search( r"^\n[^\n\S]+$", self.current_wikicode.get( insert_after_index + 1 ).value ): insert_after_index += 1 while len(self.current_wikicode.nodes) > \ (insert_after_index + 1) and \ isinstance( self.current_wikicode.get(insert_after_index + 1), mwparser.nodes.comment.Comment ): insert_after_index += 1 self.current_wikicode.insert_after( self.current_wikicode.get(insert_after_index), self.disc_notice ) # To have it in its own line we need to add a linbreak before self.current_wikicode.insert_before(self.disc_notice, "\n" ) # If there is no template, add before first element on page else: self.current_wikicode.insert( 0, self.disc_notice ) # To have it in its own line we need to add a linbreak after it self.current_wikicode.insert_after(self.disc_notice, "\n" ) # If there is no leadsec (and therefore no template in it, we will add # before the first element else: self.current_wikicode.insert( 0, self.disc_notice ) # To have it in its own line we need to add a linbreak after it self.current_wikicode.insert_after(self.disc_notice, "\n" ) # Notice was added return True def disc_notice_present(self): """ Checks if disc notice which shall be added is already present. """ if self.disc_notice in self.current_wikicode: return True # Iterate over Templates with same name (if any) to search equal # Link to decide if they are the same for present_notice in self.current_wikicode.ifilter_templates( matches=str(self.disc_notice.name) ): # Get reddisc page.title of notice to add add_notice_link_tile = self.disc_notice.get( "Diskussion").partition("#")[0] # Get reddisc page.title of possible present notice present_notice_link_tile = present_notice.get( "Diskussion").partition("#")[0] # If those are equal, notice is already present if add_notice_link_tile == present_notice_link_tile: return True # If nothing is found, loop will run till its end else: return False # We need to overrite this since orginal from pywikibot.bot.CurrentPageBot # does not return result of self._save_page def put_current(self, new_text, ignore_save_related_errors=None, ignore_server_errors=None, **kwargs): """ Call L{Bot.userPut} but use the current page. It compares the new_text to the current page text. @param new_text: The new text @type new_text: basestring @param ignore_save_related_errors: Ignore save related errors and automatically print a message. If None uses this instances default. @type ignore_save_related_errors: bool or None @param ignore_server_errors: Ignore server errors and automatically print a message. If None uses this instances default. @type ignore_server_errors: bool or None @param kwargs: Additional parameters directly given to L{Bot.userPut}. @type kwargs: dict """ # Monkey patch pywikibot.showDiff pywikibot.showDiff = showDiff if ignore_save_related_errors is None: ignore_save_related_errors = self.ignore_save_related_errors if ignore_server_errors is None: ignore_server_errors = self.ignore_server_errors return self.userPut( self.current_page, self.current_page.text, new_text, ignore_save_related_errors=ignore_save_related_errors, ignore_server_errors=ignore_server_errors, **kwargs) # We need to have a patched version to set context param to value greater 0 as # pywikibot.bot.userPut() currently does not support this value def showDiff(oldtext, newtext, context=3): """ Output a string showing the differences between oldtext and newtext. The differences are highlighted (only on compatible systems) to show which changes were made. """ PatchManager(oldtext, newtext, context=context).print_hunks()