Vés al contingut

Usuari:CobainBot/scripts/cawiki/new creators.py

De la Viquipèdia, l'enciclopèdia lliure
"""
(!C) Copyleft Coet 2023

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

AVÍS D'EXEMPCIÓ DE RESPONSABILITAT: Feu el que vos convinga en sóc l'autor però decline tota responsabilitat i no
m'importa el que feu amb la integralitat o alguna part del codi. El codi no és malintencionat i no estic disposat a
comparéixer per alguna causa derivada del seu ús. Si el conjunt o part d'eines, classes i mètodes, vos resulten útils
i podeu tornar a gastar-los no us perseguiré demanant-ne res i no m'importa si heu de canviar la llicència o autoria :)

El propòsit d'este script es poder determinar quins usuari fan un bon ús de l'eina Content Translate.

Inicialment es va dissenyar per a utilitzar-lo amb pywikibot però la complexitat per seguir el flux de les dades m'ha
obligat a desvincular-me d'este framework. He optat per mwclient, molt més lleuger i limitar-lo a la connexió, la resta
és l'API qui ens proporciona les dades i el tractament es fa per ací per a detectar qualsevol fuga de dades, excés d'ús
de memòria, etc.
"""
import locale
import os
import pickle
import re
import traceback
from argparse import ArgumentParser
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from dateutil.relativedelta import relativedelta
from pathlib import Path
from typing import List, Optional
from xml.etree.ElementTree import fromstring as get_xml_root_from_string

# mwclient
import mwclient
from mwclient import APIError

# further works
from async_creators import Creator

# globals
CONTENT_TRANSLATION_TAGS = ("contenttranslation", "contenttranslation-v2", "sectiontranslation")
REDIR_PATTERN = re.compile(r'#(?:REDIRECT|REDIRECCIÓ)', re.IGNORECASE)
WARNING_TEMPLATES = (
    "Falten referències", "FVA", "Millorar traducció", "Millorar bibliografia", "No s'entén", "Imprecís",
    "Millorar ortografia", "Expert", "Segona llegida", "Millorar introducció", "Prosa", "Millorar",
    "Millores múltiples", "Condensar", "Biaix de gènere", "Fonts primàries", "Error de gènere", "Currículum",
    "Massa vegeu també", "Millorar format", "Millorar estructura", "Duplicat", "Moure", "Moure a Viccionari",
    "Moure a Viquidites", "Moure a Viquillibres", "Moure a Viquinotícies", "Moure a Viquitexts",
    "Moure a Wikidata", "Recerca original"
)
COUNTABLE_TEMPLATES = (
    "Cal citació", "Font qüestionable", "Imprecís", "Verifica la citació", "Format ref", "Tinv"
)
DISAMBIG_TEMPLATES = (
    'Desambiguació', 'Desambigua', 'Disambig', 'DesambigCurta', 'Acrònim', 'Biografies', 'Desambiguació 2'
)


class Template:
    """
    Classe per a cada plantilla d'un article.
    Quan obtenim l'article ens interessa quedar-nos la versió XML que facilita l'extracció de plantilles.
    """
    def __init__(self, title: str):
        if title is None:
            title = '^NONE^'  # usuari va escriure {{}} Reflist i {{}} Nike a [[Umbro]]
        self.title = f'{title[0].upper()}{title[1:]}'
        self.params = {}

    def __repr__(self):
        return f'<Template {self.title}, params: {self.params}>'

    def add_param(self, key, val):
        self.params.update({key: val})

    def set_new_index(self):
        # Hi ha casos de paràmetre sense nom, però amb un =
        # Per exemple: {{sfn|Lyons et al.|1984|=228}}, es tracta d'una errada, en este cas tocava p=228
        # Per una altra banda, podria no contenir cap índex per la qual cosa hem d'assignar-li un 1.
        keys = [k for k in self.params.keys() if isinstance(k, int)]
        return max(keys) + 1 if keys else 1


class User:
    """
    Classe referent als usuaris.
    Hem d'obtenir el nombre d'articles creats per cada usuari, diferenciant aquells que s'han creat per mitjà de
    l'eina de traducció Content Translate.

    La llista d'usuaris es queda a resources al fitxer article_creators.bin. Ens quedem amb la data i hora d'inici
    de les consultes fetes per a cada usuari per acurtar les consultes a les necessàries, és a dir, començarem a demanar
    les seues contribucions al moment que es va iniciar les darreres consultes.

    En acabant, deuríem de mirar aquells articles creats que contenen avisos per si s'han intentat millorar per treure
    les plantilles de manteniment, però això igual suposa revisar totes les seues contribucions excepte aquelles que no
    tinguen més de una revisió aprofitant:
        /w/api.php?action=query&list=usercontribs&ucuser=<user>&ucprop=title|timestamp&ucshow=new|!top.
    """
    def __init__(self):
        self.id = 0
        self.name = ''
        self.edit_count = 0
        self.groups = []
        self.created_articles = 0
        self.created_disambigs = 0
        self.created_redirects = 0
        self.created_articles_by_translation_tool = 0
        self.weakly_translated_articles = 0
        self.start_time = datetime.now()
        self.end_time = None
        self.queries = 0

    def __repr__(self):
        et = ''
        if self.end_time:
            et = relativedelta(self.end_time, self.start_time)
            time = (et.hours * 3600 + et.minutes * 60 + et.seconds)
            qs = self.queries / time if time else 0
            qc = self.queries / self.creations if self.creations else 0
            et = f" {qs:0.3f} q/s {qc:0.3f} q/c [{et.days}:{et.hours}:{et.minutes}:{et.seconds}]"
        return f'<User {self.name}, ec: {self.edit_count}, cr: {self.creations} (a: {self.created_articles}, ' \
               f'r: {self.created_redirects}, d: {self.created_disambigs}) ' \
               f'ctt: {self.created_articles_by_translation_tool} wta: {self.weakly_translated_articles} ' \
               f'cr avg: {self.creation_average:.02f}, wk avg: {self.weakness_average:.02f} *** q: {self.queries}{et}>'

    @property
    def creations(self):
        return self.created_articles + self.created_redirects + self.created_disambigs

    @property
    def creation_average(self):
        if not self.created_articles:
            return 0
        return self.created_articles_by_translation_tool / self.created_articles

    @property
    def weakness_average(self):
        if not self.created_articles_by_translation_tool:
            return 0
        return self.weakly_translated_articles / self.created_articles_by_translation_tool

    def set(self, user_dict):
        self.id = user_dict['userid']
        self.name = user_dict['name']
        self.edit_count = user_dict['editcount']
        self.groups = user_dict['groups']

    def add(self, article: 'Article'):
        if article.was_redirect:
            self.created_redirects += 1
        elif article.was_disambig:
            self.created_disambigs += 1
        else:
            self.created_articles += 1

        if article.is_tool_translated:
            self.created_articles_by_translation_tool += 1
            self.check_templates(article)
            self.check_countable_templates(article)

    def check_templates(self, article: 'Article'):
        """
        De ser un article traduït amb l'eina del Content Translate comprovem que no conté alguna de les plantilles que
        avisen d'un mal ús. És a dir, si algú altre, posteriorment, ha inserit una plantilla d'avís, assumim que és
        perquè l'usuari que ha incorporat l'article no va fer tot el treball que s'ha de fer per a que l'article no
        perda qualitat.
        """
        article.templates = article.first_edit.templates if article.has_unique_revision else article.last_edit.templates
        if any(tpl.title in WARNING_TEMPLATES for tpl in article.templates):
            self.weakly_translated_articles += 1

    def check_countable_templates(self, article):
        """
        Copipastege el comentari del Xavier Dengra:

        «Com a cortesia, però sense oblidar-les, afegiria que tingui com a mínim 2 vegades la mateixa plantilla
        {{cal citació}}, {{Font qüestionable}}, {{imprecís}}, {{Verifica la citació}}, {{Format ref}} o {{tinv}}.
        O que sumi 3 cops qualsevol d'aquestes alhora.»
        """
        countable_template_titles = [tpl.title for tpl in article.templates if tpl.title in COUNTABLE_TEMPLATES]
        countable_templates_dict = {title: 0 for title in COUNTABLE_TEMPLATES}
        for title in countable_template_titles:
            countable_templates_dict[title] += 1
        # Que en tinga almenys 2 d'una de les plantilles sumatòries
        if any((x >= 2 for x in countable_templates_dict.values())):
            self.weakly_translated_articles += 1
        # Que sume 3 entre les diverses plantilles sumatòries
        elif sum(countable_templates_dict.values()) == 3:
            self.weakly_translated_articles += 1


class Revision:
    """
    Classe per a les revisions.
    Utilizem la revisió per saber-ne el contingut, les plantilles i altres propietats com els 'tags' i 'flags'.
    """
    def __init__(self):
        self.id = 0
        self.page_id = 0
        self.parent_id = 0
        self.timestamp: Optional[datetime] = None
        self.content = ''
        self.has_content = None
        self.flags: List[str] = []
        self.tags: List[str] = []
        self.templates: List[Template] = []

    def __repr__(self):
        return f'<Revision id: {self.id} has_cnt: {self.has_content} flags: {self.flags}>'

    def set(self, rev_dict):
        """
        :param rev_dict: dades rebudes de l'API
        """
        self.id = rev_dict['revid']
        self.page_id = rev_dict['pageid']
        self.parent_id = rev_dict['parentid']
        self.content = rev_dict['content']
        self.has_content = rev_dict['has_content']
        self.flags = rev_dict['flags']
        self.tags = rev_dict['tags']
        self.timestamp = rev_dict['timestamp']
        self.parse_templates()

    def parse_templates(self):
        """
        Obtenim les plantilles de l'article
        :return:
        """
        if self.has_content:
            root = get_xml_root_from_string(self.content)
            inner = root.findall('inner')
            for templ in root.findall('template'):
                title = templ.find('title').text
                template = Template(title)
                for param in templ.findall('part'):
                    name = param.find('name').text
                    index = param.find('name').attrib.get('index')
                    value = param.find('value').text
                    template.add_param(name if name else int(index) if index else template.set_new_index(), value)
                self.templates.append(template)

    def has_disambig_tpl(self):
        return any((tpl.title in DISAMBIG_TEMPLATES for tpl in self.templates))


class Article:
    """
    Classe referent als articles creats.

    De les plantilles que ens hem de quedar ha de ser la de la última revisió. Però també podria tractar-se de la
    primera en cas que s'haja creat una plana de desambiguació i no s'ha tornat mai més a modificar.

    Només considerem articles aquells que no siguen planes de desambiguació ni redireccions al moment de crear-lo.
    Si posterirorment s'aprofita la plana per a estendre-hi un article, no ens interessa ni tant sols qui ho ha fet.

    És a dir, si agafem la primera versió d'un article és per saber la intencionalitat amb la que es crea. Poder
    descartar que un usuari ha creat un article per a fer-ne una redirecció o una pàgina de desambiguació suposa
    que no li computarem l'article com a creació.

    D'altra banda necessitem la última versió per obtenir-ne les plantilles i saber si l'article necessita cap millora.
    Si l'article en qüestió és un article traduït amb l'eina del Content Translate, aleshores, potser que l'usuari
    està fent un abús de l'eina sense evitar que l'article forme part d'aquells que minven l'índex de qualitat.
    """

    def __init__(self):
        self.id = 0
        self.title = ''
        self.size = 0
        self.creation_datetime = None
        self.last_rev_datetime = None
        self.is_tool_translated = False
        self.was_redirect = False
        self.was_disambig = False
        self.is_redirect = False
        self.is_disambig = False
        self.first_edit: Optional[Revision] = None
        self.last_edit: Optional[Revision] = None
        self.has_unique_revision = False
        self.templates: List[Template] = []  # shorthand for first_edit.templates | last_edit.templates

    def __repr__(self):
        return f'<Article [[{self.title}]] id: {self.id}, cr. date: {self.creation_datetime}, unique_rev: ' \
               f'{self.has_unique_revision}, is_TT: {self.is_tool_translated}, is_redir: {self.is_redirect}, ' \
               f'was_redir: {self.was_redirect}, is_disamb: {self.is_disambig}, was_disamb: {self.was_disambig}>'

    def _set_redirect_attrs(self):
        # REDIRECTS
        if self.first_edit:
            match = REDIR_PATTERN.search(self.first_edit.content)
            self.was_redirect = bool(match)
        if self.last_edit:
            match = REDIR_PATTERN.search(self.last_edit.content)
            self.is_redirect = bool(match)
        else:
            self.is_redirect = self.was_redirect

    def _check_disambig(self):
        if self.has_unique_revision or not self.last_edit:
            self.was_disambig = self.is_disambig = self.first_edit.has_disambig_tpl()
        else:
            self.is_disambig = self.last_edit.has_disambig_tpl()
            self.was_disambig = self.first_edit.has_disambig_tpl()

    def set(self, art_dict, user: User, mwc: 'MWClient'):
        self.id = art_dict['pageid']
        self.title = art_dict['title']
        self.size = art_dict['size']
        self.creation_datetime = datetime.strptime(art_dict['timestamp'], '%Y-%m-%dT%H:%M:%SZ')
        self.has_unique_revision = all((flag in art_dict['flags'] for flag in ('new', 'top')))

        title = art_dict['title']
        # print(f'[{datetime.now():%H:%M:%S}] {user.name} [[{title}]] {art_dict["flags"]}')
        rev_dict = mwc.get_last_edit(title, user)
        user.queries += 1
        current_edit = Revision()
        current_edit.set(rev_dict)
        if current_edit.parent_id != 0:
            rev_dict = mwc.get_first_edit(title, user)
            user.queries += 1
            first_edit = Revision()
            first_edit.set(rev_dict)
            self.first_edit = first_edit
            self.last_edit = current_edit
            self.last_rev_datetime = current_edit.timestamp
        else:
            self.first_edit = current_edit

        tool_translation_tags = list(filter(lambda tag: tag in CONTENT_TRANSLATION_TAGS, self.first_edit.tags))
        self.is_tool_translated = len(tool_translation_tags) > 0

        self._set_redirect_attrs()
        self._check_disambig()


class MWClient:
    """
    Classe per a fer les consultes a l'API.
    Recorrem a mwclient després de comprovar insistentment que el framework de pywikibot no gestiona molt bé la memòria
    quan se'l du a un alt nivell d'estrés.
    """
    def __init__(self):
        self.site = mwclient.Site('ca.wikipedia.org')
        self.site.login('TronaBot', os.getenv('TRONABOTPWD'))
        self.queries = 0

    def post(self, kwargs):
        self.queries += 1
        return self.site.post(**kwargs)

    def get_all_users(self):
        """
        Mètode per a obtenir tots els usuaris amb edicions, excloent els que tinguen marca de bot.
        """
        aufrom = True  # Per superar el while...
        params = {
            "action": 'query',
            "list": 'allusers',
            "aulimit": 'max',
            "auwitheditsonly": '',
            "auprop": 'editcount|groups',
            "auexcludegroup": 'bot',
            "rawcontinue": ''
        }
        while aufrom:
            if isinstance(aufrom, str):
                params['aufrom'] = aufrom
            data = self.post(params)
            for user in data['query']['allusers']:
                yield user
            aufrom = data['query-continue']['allusers']['aufrom'] if "query-continue" in data else None

    def get_user_contribs(self, user: User):
        params = {
            'action': 'query',
            'list': 'usercontribs',
            'ucuser': user.name,
            'ucprop': 'comment|flags|ids|size|tags|timestamp|title',
            'ucshow': 'new',
            'ucnamespace': 0,
            'uclimit': 'max'
        }
        uccontinue = True
        while uccontinue:
            if isinstance(uccontinue, str):
                params['uccontinue'] = uccontinue
            data = self.post(params)
            user.queries += 1
            for art_dict in data['query']['usercontribs']:
                art_dict['flags'] = tuple(flag for flag in art_dict if flag in ('new', 'patrolled', 'anon', 'top'))
                yield art_dict
            uccontinue = data['continue']['uccontinue'] if "continue" in data else None

    def get_rev_from_title(self, title: str, user: User, first_edit: bool = True) -> dict:
        """
        Mètode per obtenir la revisió d'un article, no ens interessa obtenir el contingut ací ja que l'agafem amb el
        parsetree per escorcollar-lo mitjançant XML i facilitar l'extracció de plantilles.
        :param title: str
        :param first_edit: bool, depén de si volem la primera versió o la darrera.
        :return: OrderDict de les dades de l'API, amb algun atribut de més ;)
        """
        params = {
            'action': 'query',
            'prop': 'revisions',
            'titles': title,
            'rvlimit': 1,
            'rvslots': '*',
            'rvprop': 'comment|flags|ids|size|timestamp|tags|user',
            'rvdir': 'newer' if first_edit else 'older',
            'indexpageids': ''
        }
        data = self.post(params)
        pageid = data['query']['pageids'][0]
        rev_data = data['query']['pages'][pageid]
        revision = rev_data['revisions'][0]
        rev_data['revid'] = revision['revid']
        rev_data['timestamp'] = datetime.strptime(revision['timestamp'], '%Y-%m-%dT%H:%M:%SZ')
        rev_data['size'] = revision['size']
        rev_data['parentid'] = revision['parentid']
        rev_data['comment'] = revision['comment'] if 'comment' in revision else ''
        rev_data['tags'] = revision['tags']
        rev_data['flags'] = tuple(filter(lambda x: x in ('anon', 'minor'), revision))
        rev_data.pop('revisions')

        try:
            # Recorrem al try per evitar que l'API ens torne un error en cas que la versió està amagada.
            # Sense carregar el contingut, que suposaria més dades que no ens són útils, no podem saber
            # si podrem executar este mètode. Amb el contingut si el text està protegit apareix 'texthidden'
            rev_data['content'] = self.parse_tree(rev_data['revid'])
            rev_data['has_content'] = True
            user.queries += 1
        except APIError:
            # tornem un text buit i afegim una propietat que ens indicarà realment si l'article té contingut.
            rev_data['content'] = ''
            rev_data['has_content'] = False
        return rev_data

    def get_first_edit(self, title, user: User):
        return self.get_rev_from_title(title, user)

    def get_last_edit(self, title, user: User):
        return self.get_rev_from_title(title, user, False)

    def parse_tree(self, old_id) -> str:
        """
        Mètode per a obtenir el contingut de l'article amb format XML
        No es pot obtenir en cas que la versió estiga oculta.
        :param old_id: id de la revisió que volem
        :return: str, text amb format XML del contingut de la pàgina.
        """
        params = {
            'action': 'parse',
            'oldid': old_id,
            'prop': 'parsetree'
        }
        data = self.post(params)
        return data['parse']['parsetree']['*']

    def check_user_contribs(self, user: User):
        print(f'[{datetime.now():%H:%M:%S}] >>>> {user.name}')
        user.start_time = datetime.now()
        for art_dict in self.get_user_contribs(user):
            article = Article()
            article.set(art_dict, user, self)
            user.add(article)
            del article
        user.end_time = datetime.now()
        print(f'\n[{datetime.now():%H:%M:%S}] <<<< {user.name}')
        return user


class CreatorChecker:
    """
    Classe principal. Iterem els usuaris per a determinar quin tipus de creacions han inserit.
    Es considera creació si al moment de crear l'article:
    - no s'utilitza una redirecció
    - no s'utilitza una plantilla de desambiguació

    Si l'article s'ha creat mitjançant l'eina del Content Translate es mirarà que l'article
    no continga certes plantilles d'avisos de qualitat.
    """
    def __init__(self):
        self.start_time = datetime.now()
        self.end_time = None
        self.users: List[User] = []
        self.ready_users: List[User] = []
        self.debug = False
        self.verbose_level = 0
        self.mwc = MWClient()

    def verbose(self, message, level=6):
        if level <= self.verbose_level:
            print(message)

    # region lectura i escriptura de dades
    def show(self):
        print("\nSHOW\nUSERS")
        for _, user in enumerate(self.users, 1):
            print(f'[{datetime.now():%H:%M:%S}] {_:>3}.- {user}')
        print("\nREADY USERS")
        for _, user in enumerate(self.ready_users, 1):
            print(f'[{datetime.now():%H:%M:%S}] {_:>3}.- {user}')

    def load(self):
        """
        Mètode per obtenir les dades dels usuaris que encara no hem pogut analitzar (self.users) i aquells que ja
        tenim completats (self.ready_users).
        """
        with open('resources/article_creators.bin', 'rb') as fp:
            self.users = pickle.load(fp)

        ready_users_backup_file = Path('resources/checked_article_creators.bin')
        if ready_users_backup_file.exists():
            with open('resources/checked_article_creators.bin', 'rb') as fp:
                self.ready_users = pickle.load(fp)

    def save(self, users: Optional[List[User]] = None):
        """
        Mètode per alçar les dades dels usuaris de self.users, es pot passar una nova llista.
        """
        if not users:
            users = self.users
        with open('resources/article_creators.bin', 'wb') as fp:
            pickle.dump(users, fp, pickle.HIGHEST_PROTOCOL)

    def remove_checked_users(self):
        """
        Mètode auxiliar per descartar els usuaris a self.users que ja es troben a self.ready_users
        """
        all_users = len(self.users)
        print(f"LOADED usrs: {all_users} checked_users: {len(self.ready_users)}")
        usernames = [user.name for user in self.ready_users]
        self.users = [user for user in self.users if user.name not in usernames]
        print(f"FILTERED users: {len(self.users)}")
        # self.save()

    def backup(self):
        """
        Còpia de seguretat dels usuaris dels que ja s'ha obtingut dels dades.
        """
        with open('resources/checked_article_creators.bin', 'wb') as fp:
            pickle.dump(self.ready_users, fp, pickle.HIGHEST_PROTOCOL)
        et = relativedelta(datetime.now(), self.start_time)
        minutes = et.days * 1440 + et.hours * 60 + et.minutes
        users = len(self.ready_users)
        avg = users / minutes if minutes else 0
        et = f{avg:0.3f} u/m · queries: {self.mwc.queries} [{et.days}:{et.hours}:{et.minutes}:{et.seconds}]"
        self.verbose(f'[{datetime.now():%H:%M:%S}] BACKUP SAVED, users: {users} {et}', level=3)

    def resume(self):
        """
        pretenem reprendre la llista d'usuaris que encara no s'ha pogut escorcollar.
        """
        self.load()
        self.remove_checked_users()
        self.run_threads()
    # endregion

    def add(self, user: User):
        # self.verbose(f'[{datetime.now():%H:%M:%S}] ADDING TO BACKUP {user}', level=5)
        self.ready_users.append(user)
        self.backup()

    def initialize_users(self):
        """
        Mètode per utilitzar inicialment un sol colp i obtenir tots els usuaris. Una vegada tenim les dades de cada
        usuari no caldrà tornar a mirar aquells que no estiguen actius.
        """
        users: List[User] = []
        for usr_dict in self.mwc.get_all_users():
            user = User()
            user.set(usr_dict)
            users.append(user)
        self.users = users
        for user in users:
            print(f'[{datetime.now():%H:%M:%S}] user: {user}')

        with open('resources/all_article_creators.bin', 'wb') as fp:
            pickle.dump(users, fp, pickle.HIGHEST_PROTOCOL)

    def run_threads(self):
        if not self.users:
            self.load()
        with ThreadPoolExecutor(20) as executor:
            futures = []
            for user in self.users:
                futures.append(
                    executor.submit(
                        self.mwc.check_user_contribs, user
                    )
                )
            for future in as_completed(futures):
                try:
                    user = future.result()
                    self.add(user)
                except Exception as exception:
                    print(exception)
                    traceback.print_exc()
        self.end_time = datetime.now()
        self.verbose(f'TOTAL QUERIES: {self.mwc.queries}', level=3)
        et = relativedelta(self.end_time, self.start_time)
        self.verbose(f'Elapsed time: {et.days}d {et.hours}h {et.minutes}m {et.seconds}s', level=3)

    def run(self):
        for user in self.users:
            self.mwc.check_user_contribs(user)
            self.verbose(f"user: {user}\n", level=3)
            self.add(user)
        self.end_time = datetime.now()
        self.verbose(f'TOTAL QUERIES: {self.mwc.queries}', level=3)
        et = relativedelta(self.end_time, self.start_time)
        self.verbose(f'Elapsed time: {et.days}d {et.hours}h {et.minutes}m {et.seconds}', level=3)

    def publish_creator_page(self, limit):
        last_datetime = ''
        table = [f'{{{{/capçalera|{limit}|{datetime.now():%Y-%m-%d %H:%M}|{last_datetime}}}}}']
        with open('resources/checked_article_creators.bin', 'rb') as fp:
            users = pickle.load(fp)
        users: List[User]
        users.sort(key=lambda u: u.created_articles, reverse=True)
        i = 0
        for user in users:
            if user.created_articles == 0:
                continue
            i += 1
            cr_art = locale.format_string('%d', user.created_articles, grouping=True)
            cr_rdr = locale.format_string('%d', user.created_redirects, grouping=True)
            cr_dis = locale.format_string('%d', user.created_disambigs, grouping=True)
            table.append(f'|-\n| {i} || [[Usuari:{user.name}|]] || {cr_art} || {cr_rdr} || {cr_dis}')
            if len(table[1:]) == limit:
                break
        table.append('|}</center>')
        table.append('{{/peu}}')
        content = '\n'.join(table)
        creator_page = self.mwc.site.pages["Viquipèdia:Llista de viquipedistes per nombre d'articles creats"]
        if args.edit:
            creator_page.edit(content, 'Bot: proves')
        else:
            print(content)

    def publish_content_translate_page(self, limit):
        last_datetime = ''
        table = [f'{{{{/capçalera|{limit}|{datetime.now():%Y-%m-%d %H:%M}|{last_datetime}}}}}']
        with open('resources/checked_article_creators.bin', 'rb') as fp:
            users = pickle.load(fp)
        users: List[User]
        users.sort(key=lambda u: (u.weakness_average, -u.created_articles_by_translation_tool))
        i = 0
        for user in users:
            if user.created_articles_by_translation_tool == 0:
                continue
            i += 1
            cr_art = locale.format_string('%d', user.created_articles, grouping=True)
            ctt_cr_art = locale.format_string('%d', user.created_articles_by_translation_tool, grouping=True)
            wta = locale.format_string('%d', user.weakly_translated_articles, grouping=True)
            cr_avg = locale.format_string('%0.2f', user.creation_average, grouping=True)
            wk_avg = locale.format_string('%0.2f', user.weakness_average, grouping=True)
            table.append(
                f'|-\n| {i} || [[Usuari:{user.name}|]] || {cr_art} || {ctt_cr_art} || {wta} || {cr_avg} || {wk_avg}'
            )
            if len(table[1:]) == limit:
                break
        table.append('|}</center>')
        table.append('{{/peu}}')
        content = '\n'.join(table)
        content_translate_page = self.mwc.site.pages["Viquipèdia:Llista de viquipedistes per ús de l'eina de traducció"]
        if args.edit:
            content_translate_page.edit(content, 'Bot: proves')
        else:
            print(content)

    def publish(self):
        locale.setlocale(locale.LC_ALL, 'Catalan_Andorra.1250')
        limit = 500
        self.publish_creator_page(limit)
        self.publish_content_translate_page(limit)


class Fixtures:
    """
    Classe per resoldre conflictes i facilitar canvis en adaptar objectes d'scripts anteriors fets amb pywikibot.
    """
    @classmethod
    def fix(cls):
        with open('resources/creators_0.bin', 'rb') as fp:
            creators: List[Creator] = pickle.load(fp)
        users: List[User] = []
        for creator in creators:
            user = User()
            user.id = creator.id
            user.name = creator.name
            user.edit_count = creator.edit_count
            users.append(user)
        with open('resources/article_creators.bin', 'wb') as fp:
            pickle.dump(users, fp)

    @classmethod
    def read(cls):
        with open('resources/article_creators.bin', 'rb') as fp:
            users = pickle.load(fp)
        for user in users:
            print(user)

    @classmethod
    def remove(cls, username):
        with open('resources/article_creators.bin', 'rb') as fp:
            users = pickle.load(fp)
        for user in users.copy():
            if username == user.name:
                users.remove(user)
                print(f'user {username} removed')
                break
        with open('resources/article_creators.bin', 'wb') as fp:
            pickle.dump(users, fp)
        cls.read()

    @classmethod
    def prepare(cls):
        with open('resources/all_article_creators.bin', 'rb') as fp:
            users = pickle.load(fp)
        for _, user in enumerate(users, 1):
            print(f'{_:>5}.- {user}')


if __name__ == '__main__':
    arg_parser = ArgumentParser()
    arg_parser.add_argument('-E', '--edit', dest='edit', action='store_true')
    arg_parser.add_argument('-F', '--fix', dest='fix', action='store_true')
    arg_parser.add_argument('-f', '--filtered', dest='filtered', action='store_true')
    arg_parser.add_argument('-I', '--init', dest='init', action='store_true')
    arg_parser.add_argument('-P', '--prepare', dest='prepare', action='store_true')
    arg_parser.add_argument('-p', '--publish', dest='publish', action='store_true')
    arg_parser.add_argument('-R', '--resume', dest='resume', action='store_true')
    arg_parser.add_argument('-r', '--remove', dest='remove')
    arg_parser.add_argument('-S', '--show', dest='show', action='store_true')
    arg_parser.add_argument('-T', '--threads', dest='threads', action='store_true')
    args = arg_parser.parse_args()

    article_creators = CreatorChecker()
    if args.fix:
        Fixtures.fix()
        Fixtures.read()
    elif args.remove:
        Fixtures.remove(args.remove)
    elif args.prepare:
        Fixtures.prepare()
    elif args.init:
        article_creators.initialize_users()
    elif args.show:
        article_creators.load()
        article_creators.show()
    elif args.threads:
        article_creators.verbose_level = 4
        article_creators.run_threads()
    elif args.resume:
        article_creators.verbose_level = 4
        article_creators.resume()
    elif args.publish:
        article_creators.publish()
    elif args.filtered:
        article_creators.load()
        article_creators.remove_checked_users()