User:Veledan/FP Promoter/Source

From Wikipedia, the free encyclopedia

[edit] Main module

"""Script to help a human promote new Featured Pictures.
by Stephen Lawson (en:User:Veledan)
released under the GPL.

version 1.3beta 13-May-06
"""
import re, time
import wikipedia
from v_MyPage import MyPage as Page

try:
    set
except NameError:
    from future import set
mySite = wikipedia.Site('en')
wikipedia.setAction('FP promotion script')  #should always be overridden
# This is a purely interactive script with limited edits. Set the delay low.
wikipedia.put_throttle.setDelay(5)

#relevant pages on en wikipedia
fpPages = { 'fp'  : 'Wikipedia:Featured pictures',
            'fpt' : 'Wikipedia:Featured pictures thumbs',
            'fpv' : 'Wikipedia:Featured pictures visible',
            'fpc' : 'Wikipedia:Featured picture candidates',
            'nfp' : 'Template:Announcements/New featured pages',
            'go'  : 'Wikipedia:Goings-on',
            'arch': 'Wikipedia:Featured pictures candidates/'} #archives

#local exceptions
class ModificationFailed(wikipedia.Error):
    """Modification failed"""
class Error(Exception):
    """Promobot error"""
class DropOutError(Error):
    """Fatal error. Abandon script"""

#classes to represent the main FP pages.
#Catch wikipedia exceptions from these in the calling code
class FpPage(Page):
    """Wikipedia:Featured pictures"""
    def __init__(self):
        Page.__init__(self, mySite, fpPages['fp'])
        
    def addNewFp(self, newFp):
        """Add a new FP to WP:FP. Takes a NewFP object"""
        oldtext=self.get()
        headerRegex = newFp.fpSection.replace(' ','[ ]')
        sectionsearch = re.compile(r"""
        ={3,5}[ ]?""" + headerRegex + """[ ]?={3,5} #match the header
        .*?                                         #plus the section contents
        (?=                                         #and look ahead but don't match
        ==|<!--[ ]end[ ]of[ ]list[ ]-->             #the next header or end of list
        )""", re.DOTALL|re.VERBOSE)
        #Section headings are frequently duplicated. Work out which one to use
        sections = []
        for s in sectionsearch.finditer(oldtext):
            sections.append(s)
        if len(sections)==0:
            raise ModificationFailed('Unable to match section ' + newFp.fpSection)
        if len(sections)==2 and not newFp.createdByWikipedian:
            section = sections[1]
        else:
            section = sections[0]
        #make the new entry
        creator = newFp.creator
        if newFp.createdByWikipedian:
            creator="[[" + creator + "|]]"
            
        newEntry = "\n* '''[[:" + newFp.title() + "|" + newFp.pipedName + \
                 "]]''' at [[" + newFp.mainArticle + "]], by " + creator + "\n"
        
        #put it together
        sectiontext = section.group().rstrip()
        sectiontext += newEntry
        newtext = oldtext[:section.start()] + sectiontext + oldtext[section.end():]
        #update the FP counter
        numberofFps=len(self.imagelinks()) + 1      
        countersearch=r"are currently '''\d\d\d\d?''' featured pictures"
        countertext=r"are currently '''" + str(numberofFps) + r"''' featured pictures"
        counter=re.search(countersearch, newtext)
        if counter:
            newtext=re.sub(countersearch,countertext,newtext,1)
        editsum = 'Adding ' + newFp.title()
        self.put(newtext,editsum,minorEdit=False)
        if not counter:
            raise ModificationFailed('Image added to WP:FP ok but failed to match FP counter.')
                
    def headerlinks(self, createdByWikipedian):
        """Return a list of headers from WP:FP.
        
        Avoid ambiguity from the duplicate section names by specifying up
        front whether 'created by' or 'found by' wikipedian is appropriate.
        """
        oldtext=self.get()
        s = r'== Images created by Wikipedians ==(?P<createdby>.*?)== Images ' \
            'found by Wikipedians ==(?P<foundby>.*?)<!-- end of list -->'
        sections=re.search(s, oldtext, re.DOTALL)
        if not sections:
            raise DropOutError('ERROR: Unable to read headings from WP:FP')
        if createdByWikipedian:
            return self._listheaders(sections.group('createdby'))
        else:
            return self._listheaders(sections.group('foundby'))
    
    def _listheaders(self, text):
        """Return list of ===, ====, ===== level headers. Show level by -(s) in front"""
        headersearch=r'===.+===|====.+====|=====.+====='
        headers=[]
        for header in re.finditer(headersearch,text):
            fullheader=header.group()
            if '=====' in fullheader:
                headers.append('--' + fullheader.strip(' ='))
            elif '====' in fullheader:
                headers.append('-' + fullheader.strip(' ='))
            elif '===' in fullheader:
                headers.append(fullheader.strip(' ='))
            else:
                raise Error('debug: header regex not working')
        return headers
    
class FptPage(Page):
    """Wikipedia:Featured pictures thumbs"""
    def __init__(self):
        fptRedirPage = Page(mySite, fpPages['fpt'])
        Page.__init__(self, mySite, fptRedirPage.getRedirectTarget())
        fptRedirPage = None
    
    def addNewFp(self, newFp):
        """Add a new FP to WP:FPT. Takes a NewFP object"""
        oldtext=self.get(force=True)
        gallerystart = re.search(r'<gallery>\s*(?=Image:)',oldtext)
        if not gallerystart:
            raise ModificationFailed('Unable to locate start of gallery')
        newentry = newFp.title() + '|' + newFp.pipedName + '\n'
        newtext=oldtext[:gallerystart.end()] + newentry + oldtext[gallerystart.end():]
        editsum = "Adding " + newFp.title()
        self.put(newtext,editsum,minorEdit=False)
        
class FpvPage(Page):
    """Wikipedia:Featured pictures visible"""
    def __init__(self):
        Page.__init__(self, mySite, fpPages['fpv'])
        
    def addNewFp(self, newFp):
        """Add a new FP to WP:FPV. Takes a NewFP object"""
        oldtext=self.get()
        headerRegex = newFp.fpSection.replace(' ','[ ]')
        sectionsearch = re.compile(r"""
        ={3,5}[ ]?""" + headerRegex + """[ ]?={3,5} #match the header
        .*?                                         #plus the section contents
        (?=                                         #and look ahead but don't match
        ==|<!--[ ]end[ ]of[ ]list[ ]-->             #the next header or end of list
        )""", re.DOTALL|re.VERBOSE)
        #Section headings are frequently duplicated. Work out which one to use
        sections = []
        for s in sectionsearch.finditer(oldtext):
            sections.append(s)
        if len(sections)==0:
            raise ModificationFailed('Unable to match section ' + newFp.fpSection)
        if len(sections)==2 and not newFp.createdByWikipedian:
            section = sections[1]
        else:
            section = sections[0]
        #make the new entry
        creator = newFp.creator
        if newFp.createdByWikipedian:
            creator="[[" + creator + "|]]"
        
        if newFp.fpvSize:
            newEntry = "\n:[[" + newFp.title() + "|thumb|none|" + newFp.fpvSize \
                 + "|" + newFp.pipedName + r"<br />at [[" + newFp.mainArticle \
                 + "]] by " + creator + "]]\n"
        else:
            newEntry = "\n:[[" + newFp.title() + "|frame|none|" \
                     + newFp.pipedName + r"<br />at [[" + newFp.mainArticle \
                     + "]] by " + creator + "]]\n"
        
        #finally put it together and save
        sectiontext = section.group().rstrip()
        sectiontext += newEntry
        newtext = oldtext[:section.start()] + sectiontext + oldtext[section.end():]
        editsum = 'Adding ' + newFp.title()
        self.put(newtext,editsum,minorEdit=False)
    
    def headerlinks(self, createdByWikipedian):
        """Return a list of headers from WP:FP.
        
        Avoid ambiguity from the duplicate section names by specifying up
        front whether 'created by' or 'found by' wikipedian is appropriate.
        """
        oldtext=self.get()
        s = r'== Images created by Wikipedians ==(?P<createdby>.*?)== Images ' \
            'found by Wikipedians ==(?P<foundby>.*?)<!-- end of list -->'
        sections=re.search(s, oldtext, re.DOTALL)
        if not sections:
            raise DropOutError('ERROR: Unable to read headings from WP:FP')
        if createdByWikipedian:
            return self._listheaders(sections.group('createdby'))
        else:
            return self._listheaders(sections.group('foundby'))
    
    def _listheaders(self, text):
        """Return list of ===, ====, ===== level headers. Show level by -(s) in front"""
        headersearch=r'===[^=\n]+===|====[^=\n]+====|=====[^=\n]+====='
        headers=[]
        for header in re.finditer(headersearch,text):
            fullheader=header.group()
            if '=====' in fullheader:
                headers.append('--' + fullheader.strip(' ='))
            elif '====' in fullheader:
                headers.append('-' + fullheader.strip(' ='))
            elif '===' in fullheader:
                headers.append(fullheader.strip(' ='))
            else:
                raise Error('debug: header regex not working')
        return headers
    
class FpcPage(Page):
    """Wikipedia:Featured picture candidates"""
    def __init__(self):
        Page.__init__(self, mySite, fpPages['fpc'])
        
    def removeNomination(self, nom):
        """Remove one nomination from WP:FPC. Takes nomination Page as arg."""
        oldtext = self.get()
        editsum = 'Closing %s' % nom.title()
        #build regex to match nomination subst page
        searchexp = nom.title()
        searchexp=searchexp.replace(' ','[ _]')
        searchexp=r'{{ ?' + searchexp + r' ?}}\n|\[\[ ?' + searchexp + r' ?\]\]\n'
        if not re.search(searchexp, oldtext):
            raise Error('%s isn\'t listed at WP:FPC' % nom.title())
        newtext=re.sub(searchexp, '', oldtext)
        self.put(newtext,editsum,minorEdit=False)
        
class AnnouncePage(Page):
    """Template:Announcements/New featured pages"""
    def __init__(self):
        Page.__init__(self, mySite, fpPages['nfp'])
    
    def addNewFp(self, newFp):
        """Add a new FP to WP:FPV. Takes a NewFP object"""
        #TODO: add in auto-limiting of number of new FPs listed at any one time       
        #find the current list
        oldtext = self.get(force=True)
        fpSectionPattern = """'''\[\[Wikipedia:Featured pictures\|Pictures\]\] recently awarded "featured" status'''.*(?='''\[\[Wikipedia:Featured portals\|Portals\]\] recently)"""
        fpSection = re.search(fpSectionPattern, oldtext, re.DOTALL)
        if not fpSection:
            raise ModificationFailed('Failed to match FP section')
        #make new entry
        newentry = '* [[:' + newFp.title() + '|' + newFp.pipedName + ']] '
        newentry += '(' + unicode(time.strftime('%B %d',time.gmtime())) + ')'
        #insert and save
        sectionText = fpSection.group().rstrip()
        sectionText += '\n' + newentry + '\n\n'
        newtext = oldtext[:fpSection.start()] + sectionText + oldtext[fpSection.end():]
        editsum = 'Adding ' + newFp.title()
        self.put(newtext, editsum, minorEdit=False)
        
class GoPage(Page):
    """Wikipedia:Goings-on"""
    def __init__(self):
        Page.__init__(self, mySite, fpPages['go'])
    
    def addNewFp(self, newFp):
        """Add a new FP to WP:FPV. Takes a NewFP object"""
        #TODO: add in auto-limiting of number of new FPs listed at any one time       
        #find the current list
        oldtext = self.get(force=True)
        fpSectionPattern = """'''\[\[Wikipedia:Featured pictures\|Pictures\]\] that gained "featured" status'''.*(?='''\[\[Wikipedia:Featured portals\|Portals\]\] that)"""
        fpSection = re.search(fpSectionPattern, oldtext, re.DOTALL)
        if not fpSection:
            raise ModificationFailed('Failed to match FP section')
        #make new entry
        newentry = '* [[:' + newFp.title() + '|' + newFp.pipedName + ']] '
        newentry += '(' + time.strftime('%B %d',time.gmtime()) + ')'
        #insert and save
        sectionText = fpSection.group().rstrip()
        sectionText += '\n' + newentry + '\n\n'
        newtext = oldtext[:fpSection.start()] + sectionText + oldtext[fpSection.end():]
        editsum = 'Adding ' + newFp.title()
        self.put(newtext, editsum, minorEdit=False)

class ArchivePage(Page):
    """Wikipedia:FPC Archives"""
    def __init__(self):
        currentMonth = time.strftime('%B-%Y',time.gmtime())
        currentArchive = fpPages['arch'] + currentMonth
        Page.__init__(self, mySite, currentArchive)
        
    def addNomination(self, nom):
        """Insert one nomination from WP:FPC. Takes nomination Page as arg.
        Create new archive if necessary."""
        try:
            oldtext = self.get(force=True)
        except NoPage:
            oldtext='{{FPCArchiveBar}}\n\n'
        editsum = 'Adding %s' % nom.title()
        newtext =  oldtext + '\n{{%s}}\n' % nom.title()
        #save page
        self.put(newtext,editsum,minorEdit=False)
        
class NominationPage(Page):
    """WP:FPC/subpage for nomination."""
    
    def verdict(self, isPromoted, image=None, promotionMessage=''):
        """Add verdict to the nomination page"""
        #make verdict string & edit summary
        if isPromoted:
            verdict = '{{FPCresult|Promoted|%s}} %s ~~~~' % (image.title(), promotionMessage)
            editsum = 'Promoted'
        else:
            verdict = '{{FPCresult|Not promoted| }} %s ~~~~' % promotionMessage
            editsum = 'Not promoted'
        #insert verdict in place of reserve note
        oldtext=self.get(force=True)
        reservemarker=r'FP Promotion bot — closure.*<!-- end note -->'
        marker = re.search(reservemarker, oldtext)
        if not marker:
            raise DropOutError('ERROR: Reservation marker has gone missing. Please complete manually')
        newtext=oldtext[:marker.start()] + verdict + oldtext[marker.end():]
        #save page
        self.put(newtext, editsum, minorEdit=False)
        
    def firstName(self):
        """Return the first wikilinked username on page"""
        username=re.search(r'\[\[(User:[^\]\|]+)',self.get())
        if username:
            return username.group(1)
    
    def reserve(self):
        """Add a line to the nomination page effectively 'locking' it"""
        reservetext = r'FP Promotion bot — closure in progress. Please do ' \
                      'not amend this page while this note is showing'
        es = 'Adding reserve marker prior to closure'
        #First check not already reserved by someone else using the bot   
        oldtext = self.get() #we've already checked this exists
        reservemarker=reservetext + r'[^\[]*\[\[(?P<sig>[^\|\]]+)'
        alreadyreserved = re.search(reservemarker, oldtext)
        if alreadyreserved:
            raise DropOutError('The page has already been reserved by ' + alreadyreserved.group('sig'))
        #find where to insert our line. Any of three end markers are matched, or if not, end of str$
        endmarker=re.search(r'<!-- additional votes go above this line  -->|\{\{-\}\}|\{\{breakafterimages\}\}|$', oldtext)
        #insert reserve marker
        reservetext = '\n' + reservetext + '. ~~~~ <!-- end note -->\n\n'
        newtext = oldtext[:endmarker.start()] + reservetext + oldtext[endmarker.start():]
        #save page
        self.put(newtext, es)
    
        
#class for the image due for promotion
class NewFP(wikipedia.ImagePage):
    """An image to be promoted.
    
    Call gatherInfo() after construction.
    """
    def __init__(self, site, imageTitle, otherVersions, nominationPage):
        wikipedia.ImagePage.__init__(self, site, imageTitle)
        self.otherVersions = otherVersions # list of pages
        self.nominationPage = nominationPage
        self.nominator = None
        self.creator = None
        self.createdByWikipedian = None #boolean 
        self.fpSection = None #string
        self.fpvSection = None #string
        self.isPanorama = None
        self.isAnimated = None
        self.fpvSize = None #string, '300px'
        self.pipedName = None
        self.mainArticle = None #str title
        self.promotionMessage = None #string
        self.talkPageMessage = None #string
        self.mainVersionToReplace = None #Page
       
    def gatherInfo(self):
        #gather all required info
        self.pipedName = self._getPipedName()
        self.mainArticle = self._getArticle()
        self.mainVersionToReplace = self._getMainVersion()
        self.nominator=self._getNominator()
        self.creator, self.createdByWikipedian = self._getCreator()
        self.isPanorama, self.isAnimated, self.fpvSize = self._getSize()
        self.fpSection = self._getFpSection()
        self.fpvSection = self._getFpvSection()
        self.promotionMessage=self._getPromotionMessage()
        self.talkPageMessage=self._getTalkPageMessage()
    
    def supplantOriginalVersion(self):
        #Replace original image with self (promoted version) in articles
        editsum='Updating image after promotion of new version at %s' % self.nominationPage.title()
        for article in self.mainVersionToReplace.usingPages():
            if article.namespace()==0:
                article.put(article.get().replace(self.mainVersionToReplace.title(),self.title()),editsum)
            
    def addFpTemplate(self):
        oldtext=''
        if self.exists():
            oldtext=self.get()
        if '{{FPC}}' in oldtext:
            newtext = oldtext.replace('{{FPC}}','{{FeaturedPicture}}')
        else:
            newtext = '{{FeaturedPicture}}\n' + oldtext
        editsum = '{{FeaturedPicture}}'
        self.put(newtext,editsum)
        
    def _getPromotionMessage(self):
        return fpInput('Would you like to add any extra message to the Promotion ' \
                       'template on the nomination page? Leave this blank if not.' \
                       '\n\nIf you enter a message it will appear between the verdict ' \
                       'and your signature on the nomination page')
        
    def _getTalkPageMessage(self):
        return fpInput('Would you like to add any extra message to the Promotion ' \
                       'template on the nominator\'s and/or creator\'s talk page?' \
                       'Leave this blank if not.\n\nIf you enter a message it ' \
                       'will appear between the promotion template and your ' \
                       'signature on the nomination page')
        
    def _getArticle(self):
        while True:
            choice = fpInput('What is the title of this image\'s main article?')
            choice = unicode(choice)
            page = Page(mySite, choice)
            if page.exists():
                return page.title()
            output('That page doesn\'t exist. Try again')
    
    def _getMainVersion(self):
        replace = letteredquestion('Is the promoted image an edit that should replace '
                                   'the original version in mainspace articles?')
        if replace=='[n]o':
            return None
        oldversion=numberedquestion('Which was the original version?\n\nThe image you '
                                'select will be replaced in mainspace articles '
                                'with ' + self.title(), [i.title() for i in self.otherVersions])
        return wikipedia.ImagePage(mySite,oldversion)    
    
    def _getPipedName(self):
        return fpInput('What wording should be used in piping text links to the ' 
                       'image? (e.g. Image:blue_bird02_edit.jpg might be "Blue bird")')
        
    def _getSize(self):
        choice=letteredquestion('Is this a wide panorama? (This affects the size ' 
                                'it will be shown at in Wikipedia:Featured ' 
                                'pictures visible)')
        isPanorama = (choice=='[y]es')
        choice=letteredquestion('Is this an animated gif? (These are not resized)')
        isAnimated = (choice=='[y]es')
        if isPanorama:
            size='600px'
        elif isAnimated:
            size=''
        else:
            size='300px'
        return isPanorama, isAnimated, size
        
    def _getFpSection(self):
        question = 'What section should this be listed under at Wikipedia:Featured pictures?'
        return numberedquestion(question, fp.headerlinks(self.createdByWikipedian)).strip(' -')
        
    def _getFpvSection(self):
        question = 'What section should this be listed under at Wikipedia:Featured pictures visible?'
        if self.isAnimated or self.isPanorama:
            question += '\n\nYour previous answers indicate this is a panorama ' \
                        'or animation. Please choose the matching section as the ' \
                        'image will be sized differently from normal.'
        return numberedquestion(question,fpv.headerlinks(self.createdByWikipedian)).strip(' -')
    
    def _getNominator(self):
        probableNominator = self.nominationPage.firstName()
        while True:
            question='Which wikipedian nominated the image? (enter username, ' \
                     'not nickname)'
            if probableNominator:
                question+='\n\nYou can leave this blank if it was ' + probableNominator
            choice = fpInput(question)
            if choice.strip()=='':
                choice=probableNominator
            if ':' not in choice:
                choice='User:' + choice
            userpage = Page(mySite, choice)
            try:
                userpage.get()
            except wikipedia.Error:
                choice=letteredquestion('Page %s doesn\'t exist. Please confirm ' \
                    'the username was correct (in which case the bot will ' \
                    'create the talk page if necessary when leaving a message) ' \
                    'or choose No to re-enter the name:' % userpage.title())
            else:
                choice='[y]es'
            if choice=='[y]es':
                break
        return userpage.title()
            
    def _getCreator(self):
        choice = letteredquestion('Was %s also the creator of the image?' % self.nominator)
        if choice=='[y]es':
            return self.nominator, True
        choice = letteredquestion('Is the creator of the image a wikipedian?')
        createdByWikipedian = (choice=='[y]es')
        if createdByWikipedian:
            question="Please enter the username of the wikipedian who created" \
                     "the image (with or without the \'User:\')"
        else:
            question="Please give a name or other suitable attribution for " \
                     "the creator of the image (e.g. 'John Smith' or 'NASA')"
        while True:       
            choice = fpInput(question)
            if not createdByWikipedian:
                creator = choice
                break #no validation in that case
            if ':' not in choice:
                choice='User:' + choice
            userpage = Page(mySite, choice)
            try:
                userpage.get()
            except wikipedia.Error:
                choice=letteredquestion('Page %s doesn\'t exist. Please confirm ' \
                    'the username was correct (in which case the bot will ' \
                    'create the talk page if necessary when leaving a message) ' \
                    'or choose No to re-enter the name:' % userpage.title())
            else:
                choice='[y]es'
            if choice=='[y]es':
                creator = userpage.title()
                break
        return creator, createdByWikipedian

#main control class
class FpPromoter(object):
    def __init__(self):
        #initialize page objects if necessary
        try:
            fp
        except NameError:
            createGlobalPageObjects()
        #instance variables
        self.nomPage=None  #Page
        self.newFp=None    #Page
        self.promoted=None #boolean
        self.report=['== FP Promotion Tool started =='] #List of outcomes
    
    def run(self):
        """Main control procedure."""
        output('== FP Promotion Tool started ==')
        #Ask which nomination to close
        self.nomPage = self._getNominatedPage()
        self._report('%s selected' % self.nomPage.title())
        #Reserve the page to stop edit conflicts       
        output('Attempting to reserve nomination page...')
        try:
           self.nomPage.reserve()
        except wikipedia.Error, error:
            raise DropOutError(error)
        self._report('Reserve marker added to page')
        #Ask whether the image is promoted or not
        self.promoted = self._getResult()
        #promote or not promote
        if self.promoted:
            self.promote()
        else:
            self.dontpromote()
    
    def promote(self):
        """Perform the steps to close a nomination and promote an image"""
        #find out which image is to be promoted
        images=self.nomPage.imagelinks()
        imagelist = [i.title() for i in images]
        promotedimage = numberedquestion('Which image to promote?',imagelist)
        #make list of imagepages not being promoted
        otherimages = set([i for i in images if i.title() != promotedimage])
        #create newFp object
        self.newFp = NewFP(mySite, promotedimage, otherimages, self.nomPage)
        #new FP will ask the rest of the questions
        self.newFp.gatherInfo()
        #PROMOTION STEPS
        #1. Add verdict
        output('Adding verdict to nomination page...')
        try:
           self.nomPage.verdict(True, self.newFp, self.newFp.promotionMessage)
        except wikipedia.Error, error:
            raise DropOutError('ERROR: Failed to add verdict: ' + str(error))
        else:
            self._report('Promoted. Verdict added to nomination page')
        #2. Add to archive
        output('Adding entry to %s archive...' % time.strftime('%B',time.gmtime()))
        try:
            arc.addNomination(self.nomPage)
        except wikipedia.Error, error:
            self._report('Failed to add entry to archive: ' + str(error))
        else:
            self._report('Added entry to archive OK')
        output('Removing entry from WP:FPC...')
        try:        
            fpc.removeNomination(self.nomPage)
        except wikipedia.Error, error:
            self._report('Failed to remove entry from WP:FPC: ' + str(error))
        except Error, error:
            self._report(str(error))
        else:
            self._report('Removed from WP:FPC OK')
        #3. Add to New Featured content template
        output('Adding to Template:Announcements/New featured pages...')
        try:
            nfp.addNewFp(self.newFp)
        except wikipedia.Error, error:
            self._report('Failed to add entry to Template:Announcements/New featured pages: ' + str(error))
        else:
            self._report('Added to Template:Announcements/New featured pages OK')
        #4. Add to Goings-on
        output('Adding to Wikipedia:Goings-on...')
        try:
            goo.addNewFp(self.newFp)
        except wikipedia.Error, error:
            self._report('Failed to add entry to Wikipedia.Goings-on: ' + str(error))
        else:
            self._report('Added to Wikipedia:Goings-on OK')
        #5. Add to Featured Pictures
        output('Adding to WP:FP...')
        try:
            fp.addNewFp(self.newFp)
        except wikipedia.Error, error:
            self._report('Failed to add entry to Wikipedia:Featured pictures: ' + str(error))
        else:
            self._report('Added to Wikipedia:Featured pictures OK')
        #6. Add to WP:FPV
        output('Adding to WP:FPV...')
        try:
            fpv.addNewFp(self.newFp)
        except wikipedia.Error, error:
            self._report('Failed to add entry to Wikipedia:Featured pictures visible: ' + str(error))
        else:
            self._report('Added to Wikipedia:Featured pictures visible OK')
        #7. Add to WP:FPT
        output('Adding to WP:FPT...')
        try:
            fpt.addNewFp(self.newFp)
        except wikipedia.Error, error:
            self._report('Failed to add entry to Wikipedia:Featured pictures thumbs: ' + str(error))
        else:
            self._report('Added to Wikipedia:Featured pictures thumbs OK')
        #8. Update {{FPC}} tags
        output('Adding {{FeaturedPicture}}...')
        try:
            self.newFp.addFpTemplate()
        except wikipedia.Error, error:
            self._report('Failed to add {{FeaturedPicture}}: ' + str(error))
        else:
            self._report('Added {{FeaturedPicture}} OK')
        self._report('Removing {{FPC}} from all other images in Nom page...')
        for image in self.newFp.otherVersions:
            newtext=''
            if not image.exists(): #no need to remove from empty page
                continue
            oldtext=image.get()
            if '{{FPC}}' in oldtext:
                newtext=oldtext.replace('{{FPC}}','')
                try:
                    image.put(newtext,'Removing {{FPC}}')
                except wikipedia.Error:
                    self._report('Failed to remove {{FPC}} from ' + image.title())
                    continue
                self._report('{{FPC}} removed from ' + image.title())
        #9. Notify nominator
        output('Notifying nominator...')
        editsum='New featured pic'
        try:
            nominatorsTalkPage=Page(mySite,self.newFp.nominator).switchTalkPage()
            newmessage='\n==Featured picture promotion==\n{{subst:PromotedFPC|%s}}<br />%s ~~~~' % (self.newFp.title(), self.newFp.talkPageMessage)
            nominatorsTalkPage.put(nominatorsTalkPage.get()+newmessage,editsum,minorEdit=False)
        except wikipedia.Error, error:
            self._report('Failed to leave message for nominator: ' + str(error))
        else:
            self._report('Notified nominator OK')
        #10. Notify creator
        if self.newFp.nominator != self.newFp.creator and self.newFp.createdByWikipedian:
            output('Notifying creator...')
            editsum='New featured pic'
            try:
                creatorsTalkPage=Page(mySite,self.newFp.creator).switchTalkPage()
                newmessage='\n==Featured picture promotion==\n{{subst:UploadedFP|%s}}<br />%s ~~~~' % (self.newFp.title(), self.newFp.talkPageMessage)
                creatorsTalkPage.put(creatorsTalkPage.get()+newmessage,editsum,minorEdit=False)
            except wikipedia.Error, error:
                self._report('Failed to leave message for nominator: ' + str(error))
            else:
                self._report('Creator nominator OK')
        self._report('Promotion completed')
        #11. Replace original image with edited version if necessary
        if self.newFp.mainVersionToReplace:
            output('Replacing image in articles...')
            self.newFp.supplantOriginalVersion()
        
    def dontpromote(self):
        """Perform the steps to close a nom with no promotion"""
        #add verdict
        message = fpInput('Would you like to add any extra message to the non-Promotion ' \
                       'template on the nomination page? Leave this blank if not.' \
                       '\n\nIf you enter a message it will appear between the verdict ' \
                       'and your signature on the nomination page')
        output('Adding verdict to nomination page...')        
        try:
            self.nomPage.verdict(False, None, message)
        except wikipedia.Error, error:
            raise DropOutError('ERROR: Failed to add verdict:' + str(error))
        else:
            self._report('Not promoted. Verdict added to nomination page')
        #remove {{FPC}} from versions
        self._report('Removing {{FPC}} from all images in Nom page...')
        images = set(self.nomPage.imagelinks())
        for image in images:
            newtext=''
            if not image.exists(): #no need to remove from empty page
                continue
            oldtext=image.get()
            if '{{FPC}}' in oldtext:
                newtext=oldtext.replace('{{FPC}}','')
                try:
                    image.put(newtext,'Removing {{FPC}}')
                except wikipedia.Error:
                    self._report('Failed to remove {{FPC}} from ' + image.title())
                    continue
                self._report('{{FPC}} removed from ' + image.title())
        #Remove from Wp:FPC
        output('Removing entry from WP:FPC...')
        try:        
            fpc.removeNomination(self.nomPage)
        except wikipedia.Error, error:
            self._report('Failed to remove entry from WP:FPC: ' + str(error))
        except Error, error:
            self._report(str(error))
        else:
            self._report('Removed from WP:FPC OK')
        #Add to archive
        output('Adding entry to %s archive' % time.strftime('%B',time.gmtime()))
        try:
            arc.addNomination(self.nomPage)
        except wikipedia.Error, error:
            self._report('Failed to add entry to archive: ' + str(error))
        else:
            self._report('Added entry to archive OK')
        self._report('Non-promotion completed')
        
    def _report(self, message, echo=True):
        self.report.append(message)
        if echo:
            output(message)
        
    def _getResult(self):
        return (letteredquestion('Has %s resulted in a promotion?' % self.nomPage.title())=='[y]es')
        
    def _getNominatedPage(self):
        while True:
            choice = fpInput("What page to work on? Please enter the full title " \
                        "pasted from the subpage, e.g. 'Wikipedia:Featured picture " \
                        "candidates/Street Trinidad Cuba'")
            nomPage=NominationPage(mySite, choice)
            if nomPage.exists():
                break
            else:
                output('Error: %s doesn\'t exist. Please try again...' % choice)
        return nomPage
        

#functions for use by all classes
def fpInput(message):
    output('\n' + message)
    answer=raw_input('--> ')
    answer = unicode(answer.strip(),'utf-8')
    output(answer,logonly=True)
    output('\n\n')
    return answer

def output(message, logonly=False):
    if not logonly:
        wikipedia.output(unicode('\n' + message))
    #also write to log if there is one
    try:
        promobotlogfile
    except NameError: pass
    else:
        promobotlogfile.write(time.strftime('[%d%b%y %H:%M:%S] ',time.gmtime()) + message + '\n')

def numberedquestion(message, answers):
    """Show a numbered list of 'answers' and return the selected item"""
    if message[:-1] != '\n':
        message+='\n'
    for item in answers:
        message+=('%u. ' % (answers.index(item) + 1)) + item + '\n'
    while True:
        choice = fpInput(message)
        try:
            choice=int(choice)
        except ValueError:
            choice=0
        if choice > len(answers) or choice < 1:
            output('ERROR: You have to choose a number from the menu. Try again.\n')
        else:
            return answers[choice-1]
    
def letteredquestion(message, answers=['[y]es','[n]o']):
    """Show a list of options with [s]ignicant letters and return selected item"""
    if message[:-1] != '\n':
        message+='\n'
    answerlist=''
    for item in answers:
        message+=(' - ') + item + '\n'
        keyletter=re.search(r'\[([^\[\]])\]', item).group(1).lower()
        if keyletter=='':
            raise DropOutError('Bad list of possible answers - no [] in %s' % item)
        answerlist+=keyletter
    while True:
        choice=fpInput(message).lower()[:1]
        if not choice or choice not in answerlist:
            output('ERROR: You have to choose a letter from the menu. Try again.\n')
        else:
            return answers[answerlist.index(choice)]

def createGlobalPageObjects():
    global fp, fpc, fpv, fpt, nfp, goo, arc
    fp = FpPage()
    fpv = FpvPage()
    fpc = FpcPage()
    fpt = FptPage()
    nfp = AnnouncePage()
    goo = GoPage()
    arc = ArchivePage()

def createLogFile():
    global promobotlogfile
    promobotlogfile = file('promobotlog.txt','a')
def closeLogFile():
    promobotlogfile.close()

if __name__=='__main__':
    try:
        createLogFile()
        wikipedia.Site.forceLogin(mySite)
        #make global FP page objects   
        fp = FpPage()
        fpv = FpvPage()
        fpc = FpcPage()
        fpt = FptPage()
        nfp = AnnouncePage()
        goo = GoPage()
        arc = ArchivePage()
        #go
        try:
            promobot = FpPromoter()
            promobot.run()
        except DropOutError, error:
            output('\nThe script was terminated early. Reason::')
            output(str(error) + ' -- Please complete manually')
    finally:
        wikipedia.stopme()
        #if the report exists, output it
        try:
            promobot.report
        except:
            pass
        else:
            output('=================================')            
            output('Progress report::')
            output('\n'.join(promobot.report))
        closeLogFile()
        

[edit] v_MyPage.py

"""Class wikipedia.Page is overridden because the regular expression in the
original fails to match image links that have external links embedded in
their captions, and mismatches image links with wikilinks in their captions
"""
import wikipedia, re

class MyPage(wikipedia.Page):
    #def __init__(self, site, title):
    #    """temp, for testing fp promotor macro"""
    #    if 'Wikipedia:Sandbox/Veledan/' not in title:
    #        title='Wikipedia:Sandbox/Veledan/' + title
    #    wikipedia.Page.__init__(self, site, title)
    
    def linkedPages(self):
        """Gives the normal (not-interwiki, non-category) pages the page
           links to, as a list of Page objects
           Amended regex to spot links to images with an external link in 
           their caption, hence overwritten
        """
        result = []
        try:
            thistxt = wikipedia.removeLanguageLinks(self.get())
        except NoPage:
            return []
        except IsRedirectPage:
            raise
        thistxt = wikipedia.removeCategoryLinks(thistxt, self.site())
        
        Rlink = re.compile( \
            r'''\[\[            #opening wikilink brackets
            (?P<title>[^\]\|]*)   #title=everything up to first ] or |
            (                   #then an optional, repeatable group
            [^\]\[]             #containing at least one non-bracket char OR
            |
            \[[^\[\]]*\]        #an embedded external link with both brackets [...] OR
            |
            \[\[[^\[\]]*\]\]    #an embedded complete wikilink [[...]] 
            )*                  #all that zero or more times
            \]\]                #until ]] is reached. This will not be matched by above expression
            ''', re.VERBOSE)
        for match in Rlink.finditer(thistxt):
            title = match.group('title')
            if not self.site().isInterwikiLink(title):
                page = MyPage(self.site(), title)
                result.append(page)
        return result