You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

357 lines
12 KiB

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 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
# 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 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 ):
@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
# Run super class init with builded generator
super( MarkPagesBot, self ).__init__(
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
super( MarkPagesBot, self ).run()
# Do status redfam status updates
for redfam in self.redfams:
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(
"%Y-%m-%d" )
if hasattr(self, "famhash"):
self.__redfams = list(
RedFamWorker.famhash == self.famhash ) )
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(
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 ):
elif save_ret is None:
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 = \
# Check if it is already present in wikicode
if self.disc_notice_present():
# 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 \ 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 \
self.current_wikicode.get(insert_after_index + 1),
mwparser.nodes.comment.Comment ):
insert_after_index += 1
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
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
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( ):
# Get reddisc page.title of notice to add
add_notice_link_tile = self.disc_notice.get(
# Get reddisc page.title of possible present notice
present_notice_link_tile = present_notice.get(
# 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
return False
# We need to overrite this since orginal from
# 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,
# We need to have a patched version to set context param to value greater 0 as
# 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()