Usuari:CobainBot/scripts/cawiki/AdQ.py
Aparença
##############################################################################################################
# 4a versió #
# Programa que arxiva les propostes de distinció de qualitat i classifica els articles com AdQ o bo #
# IMPORTANT!!! ---> No està provat amb articles que ja hagin passat una avaluació. Si ho feu, reviseu-ho tot #
# #
# Autor:Gerardduenas #
# Uniformitzat per Coet, a 14-ag-2015 (versió anterior a museum/AdQ.py) #
# Py2 > Py3 by Coet, at 2023-05-20. #
##############################################################################################################
import html.parser as html_parser
import locale
import re
import sys
import time
from argparse import ArgumentParser
import pywikibot
from dateutil.relativedelta import relativedelta
from datetime import date, datetime
from dataclasses import dataclass
from typing import Dict, List, Literal, NoReturn, Optional
from pywikibot import Page, Site, bot
from pywikibot.data import api
"""
Automatització de l'arxivament de propostes a Article de Qualitat/Bo [20/05/2023]
El bot es dirigix a [[Viquipèdia:Articles de qualitat]] i ha d'obtenir aquelles propostes que continguen la plantilla
{{AdQ per arxivar}}. La plantilla conté quatre paràmetres:
- Categoria
- Subapartat
- Resultat proposta
- és una traducció.
De la proposta ha d'extreure:
- nom de l'article
- proponent
- avaluació prèvia
- data
- plantilla {{AdQ per arxivar|...}} o {{ABo per arxivar|...}}
El bot haurà de modificar:
- La pàgina de l'article
- La pàgina de discussió de l'article
- La pàgina principal [[Viquipèdia:Proposta de distinció d'alta qualitat d'un article]]
- La subpàgina [[Viquipèdia:Articles <de qualitat|bons>/<Categoria>]]
- La subpàgina de la proposta [[Viquipèdia:Proposta de distinció d'alta qualitat d'un article/<Article>]]
- La subpàgina de la proposta anual [[Viquipèdia:Proposta de distinció d'alta qualitat d'un article/<Any>]]
- La subpàgina [[Viquipèdia:Articles <de qualitat|bons>/Llista cronològica]]
- La plantilla de les darreres propostes resoltes [[Plantilla:Darreres propostes resoltes]]
El bot accedirà a:
- [[Viquipèdia:Articles de qualitat]] per obtenir les categories dels AdQ
- [[Viquipèdia:Articles bons]] per obtenir les categories dels ABons
- [[Plantilla:Darreres propostes resoltes]] per obtenir el nombre de propostes recents a mostrar.
Possibles errors:
- la plantilla {{AdQ per arxivar}} no inclou la categoria
- la categoria de la plantilla {{AdQ per arxivar}}
- inclou una categoria que no existeix
- inclou un subapartat que no existeix
- el resultat no coincideix amb el recompte de vots
- no s'indica si és una traducció.
Normes:
Les trobem a [[Viquipèdia:Proposta_de_distinció_d'alta_qualitat_d'un_article]]
Un article ha d'haver estat un mínim de dues setmanes en avaluació per a ser candidat a una distinció.
- A [[Viquipèdia:Avaluació d'articles/<Article>]] Hi trobem la data d'avaluació
vots posibles: {{votqualitat}}, {{votbo}}, {{objecció}}, {{nsnc}}.
vot modificat: <s[trike]>{{vot}}</s[trike]>
usuari actiu amb una antiguitat mínima d'un mes i 100 contribucions.
Assoliment:
- QUALITAT:
- 8 vots sense cap en contra [votbo, objecció]
- 10 vots dels quals 8 són favorables [votqualitat]
- BONS:
- 5 vots, cap en contra
- 6 vots, un en contra
- 10 vots, 2 objeccions
Periode:
A banda de les dues setmanes d'avaluació no hi ha periode mínim
No hi ha nombre de dies. Es recomana 3 o 4 dies si el mínim de vots s'assolira abans.
Quorum:
Si porta 3 mesos només necessita 7 vots favorables, cap en contra
Si porta 4 mesos només necessita 6 vots favorables, cap en contra
Si porta 5 mesos només necessita 5 vots favorables, cap en contra
Si porta 6 mesos amb algun vot negatiu, no es pot aprovar.
"""
class Months:
def __init__(self):
self._wiki_months = [short for long, short in site.months_names]
self._sys_months = [time.strftime("%b", time.strptime(f"01-{_:>02}-2023", "%d-%m-%Y")) for _ in range(1, 13)]
self.month_dict = dict(zip(self._wiki_months, self._sys_months))
def get(self, wiki_month) -> str:
return self.month_dict.get(wiki_month)
def from_wiki_text(self, text) -> Optional[date]:
match = re.search(r'(?P<date>\d{2}:\d{2}, \d{1,2} (?P<month>\w{3,4}) \d{4}) \(CES?T\)', text)
if match:
wiki_month = match.group('month')
date_str = match.group('date').replace(wiki_month, self.get(wiki_month))
return datetime.strptime(date_str, '%H:%M, %d %b %Y')
@dataclass
class Vote:
award: str = ''
date: datetime = None
user: 'User' = None
valid: bool = False
class Display:
line_sep = "####################################################################\n"
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
@classmethod
def _color_text(cls, text, rgb):
r, g, b = rgb
return f"\033[38;2;{r};{g};{b}m{text}\033[0m"
@classmethod
def show(cls, new_text, header, location=None):
print(cls._color_text(cls.line_sep, cls.GREEN))
print(cls._color_text(header, cls.GREEN))
if location:
match = re.search('(?P<begin>.*)<<(?P<title>[^>]+)>>(?P<end>.*)', location)
if match:
location_begin = cls._color_text(match.group('begin'), cls.YELLOW)
title = cls._color_text(f'[[{match.group("title")}]]', cls.RED)
location_end = '' if not match.group('end') else cls._color_text(match.group('end'), cls.YELLOW)
print(f'{location_begin}{title}{location_end}')
else:
print(cls._color_text(location, cls.YELLOW))
print(new_text)
print(cls._color_text(cls.line_sep, cls.RED))
@classmethod
def begin(cls, proposal: 'Proposal'):
ln = cls._color_text(cls.line_sep[:-1], cls.BLUE)
txt = cls._color_text(f"ARXIVANT LA PROPOSTA de l'article", cls.GREEN)
art = cls._color_text(f"[[{proposal.title}]]", cls.RED)
print(f"\n{ln} {txt} {art} {ln}\n")
class User:
def __init__(self, name):
self.id = 0
self.name: str = name
self.edit_count: int = 0
self.last_contrib: Optional[datetime] = None
self.run()
def __repr__(self):
return f"<User {self.id}, name: {self.name}, editcount: {self.edit_count}, " \
f"last_edit: {self.last_contrib:%Y-%m-%d %H:%M}>"
@staticmethod
def _query(params) -> dict:
qry = api.Request(site, parameters=params)
return qry.submit()
def set_last_contrib(self):
params = {
'action': 'query',
'list': 'usercontribs',
'ucuser': self.name,
'ucprop': 'timestamp',
'uclimit': 1
}
data = self._query(params)
user_dict = data['query']['usercontribs'][0]
self.last_contrib = datetime.strptime(user_dict['timestamp'], '%Y-%m-%dT%H:%M:%SZ')
return data['query']['usercontribs'][0]['timestamp']
def get_last_contrib_before_dt(self, dt: datetime):
ts = datetime.strftime(dt, '%Y-%m-%dT%H:%M:%SZ')
params = {
'action': 'query',
'list': 'usercontribs',
'ucuser': self.name,
'ucprop': 'timestamp',
'ucend': ts,
'uclimit': 1
}
data = self._query(params)
user_dict = data['query']['usercontribs'][0]
last_contrib = user_dict['timestamp']
return datetime.strptime(last_contrib, '%Y-%m-%dT%H:%M:%SZ')
def set_contribs(self):
# action=query&list=users&ususers=Coet&usprop=editcount
params = {
'action': 'query',
'list': 'users',
'usprop': ('editcount',),
'ususers': self.name
}
data = self._query(params)
user_dict = data['query']['users'][0]
self.id = user_dict['userid']
self.edit_count = user_dict['editcount']
def run(self):
self.set_contribs()
self.set_last_contrib()
class Proposal:
"""
Classe per a emmagatzemar les dades d'una proposta.
"""
def __init__(self, title: str, author: str, date_str: str, votes: str):
self.page = Page(site, title)
self.title: str = title
self.nominator: Optional[str] = author
self.nomination_page: Page = Page(site, f"Viquipèdia:Avaluació d'articles/{title}")
self.award: Optional[Literal['qualitat', 'bo']] = None
self.category: str = ''
self.proposal_date: date = datetime.strptime(date_str, '%d-%m-%Y')
self.nomination_date: Optional[date] = None
self._votes: str = votes
self.votes: List[Vote] = []
self.first_tpl = r"#{{ArxiuAdQ|"
self.approved: Optional[bool] = None
self.translated: Optional[bool] = None
self.pattern = re.compile(
r"#\s*(?P<strike><s(?:trike)?>)?{{\s*(?P<vote>[Vv]ot(?P<award>bo|qualitat)?|objecció|nsnc)}}.*"
r"(?P<date>\d{2}:\d{2}, \d{1,2} (?P<month>\w{4}|\w{2,3}\.?) \d{4}) \(CES?T\)\s*(?P<ekirts></s(?:trike)?>)?",
re.UNICODE
)
self.parse_votes()
self.set_nomination_date()
@property
def title_without_html_entities(self):
return html_parser.unescape(self.title)
def __repr__(self):
return f"<Proposal [[{self.title}]], nominator: {self.nominator}, date: {self.proposal_date:%Y-%#m-%#d}, " \
f"award: {self.award} {len(self.votes)} votes>"
@property
def award_tpl(self):
return f"{{{{vot{self.award}}}}}"
@property
def color(self):
return 'green' if self.approved else 'red'
@property
def result(self):
return f"'''{'Aprovat' if self.approved else 'No aprovat'}'''"
@property
def day(self) -> int:
return self.proposal_date.day
@property
def month(self) -> str:
return self.proposal_date.strftime("%B")
@property
def year(self):
return self.proposal_date.strftime("%y")
@property
def full_year(self) -> str:
return self.proposal_date.strftime("%Y")
@property
def of(self) -> str:
return "d'" if self.month.startswith('a') or self.month.startswith('o') else 'de '
@property
def exposure_period(self):
return relativedelta(self.proposal_date, self.nomination_date)
@property
def exposure_period_is_valid(self):
days = self.exposure_period.days
months = self.exposure_period.months
return months > 0 or (months == 0 and days >= 14)
def parse_votes(self):
counter = {
'qualitat': 0,
'bo': 0,
'objecció': 0,
'nsnc': 0
}
for match in self.pattern.finditer(self._votes):
award = match.group('award')
wiki_month = match.group('month')
date_str = match.group('date').replace(wiki_month, months.get(wiki_month))
vote = Vote()
vote.award = award
vote.date = datetime.strptime(date_str, '%H:%M, %d %b %Y')
self.votes.append(vote)
counter[award] += 1
self.award = 'qualitat' if counter['qualitat'] > counter['bo'] else 'bo'
def set_nomination_date(self):
text = self.nomination_page.text
self.nomination_date = months.from_wiki_text(text)
class ProposalProvider:
"""
Classe per a emmagatzemar les diferents propostes.
"""
def __init__(self):
self.debug: bool = debug
self.proposal_list: List[Proposal] = []
self.page = Page(site, "Viquipèdia:Proposta de distinció d'alta qualitat d'un article")
self.page_pattern = re.compile("== Propostes de distinció actuals ==(?P<proposals>.*)==", re.DOTALL)
self.subpage_pattern = re.compile(
r"=== \[\[[^]]+]] ===\s*\* '''Article''': {{DadesArticle\|(?P<title>[^}]*)}}\s*\* "
r"'''Proponent''': \[\[Usuari:(?P<nominator>[^]]+)]]\s*\* "
r"'''Avaluació prèvia:''' \[\[Viquipèdia:Avaluació d'articles/[^]]+]]\s*\* "
r"'''Data''': (?P<date>\d{1,2}-\d{1,2}-\d{4}).+"
r"'''Valoracions'''(?P<votes>.*)\[\[Categoria:Propostes d'articles de qualitat\|",
re.DOTALL
)
self.proposal_pattern = re.compile(r"----.*?{{/(?P<title>[^}]+)}}", re.DOTALL)
self.dispatch()
def __iter__(self):
return self.proposal_list.__iter__()
def get_by_name(self, title) -> Optional[Proposal]:
filtered_proposals = [p for p in self.proposal_list if p.title == title]
return filtered_proposals[0] if filtered_proposals else None
def dispatch(self):
text = self.page.text
match = self.page_pattern.search(text)
if match:
for proposal in self.proposal_pattern.finditer(match.group('proposals')):
title = proposal.group('title')
page = Page(site, f"Viquipèdia:Proposta de distinció d'alta qualitat d'un article/{title}")
match = self.subpage_pattern.search(page.text)
if match:
title = match.group('title')
nominator = match.group('nominator')
date_str = match.group('date')
votes = match.group('votes')
new_proposal = Proposal(title, nominator, date_str, votes)
self.proposal_list.append(new_proposal)
if self.debug and self.proposal_list:
for proposal in self.proposal_list:
print(proposal)
class ProposalSubpage:
"""
Classe mare per a les propostes.
Les subpàgines contenen les propostes.
"""
def __init__(self):
self.debug: bool = debug
self.main_title = "Viquipèdia:Proposta de distinció d'alta qualitat d'un article"
self.page: Optional[Page] = None
self.proposal: Optional[Proposal] = None
def show(self, new_text: str, header: str = '', location: str = ''):
Display.show(new_text, header, location)
class ArticleProposalSubpage(ProposalSubpage):
"""
Subpàgina de la classe mare que conté la proposta.
"""
def __init__(self):
ProposalSubpage.__init__(self)
def run(self, prop: Proposal):
# Categories
self.proposal = prop
self.page = Page(site, f"{self.main_title}/{prop.title}")
old_text = self.page.text
old_cat = "Categoria:Propostes d'articles de qualitat"
new_cat = "Categoria:Propostes d'AdQ ({{subst:CURRENTYEAR}})"
new_text = old_text.replace(old_cat, new_cat)
if self.debug:
self.show(
new_text,
"Les categories han quedat així",
f'... a la subpàgina de la proposta <<{self.page.title(without_brackets=True)}>>'
)
return
bot.output('Canviant categories...')
self.page.text = new_text
self.page.save("Bot arxivant la proposta")
class ArchiveProposalSubpage(ProposalSubpage):
"""
Subpàgina de la classe mare que conté l'arxivament de la proposta.
"""
def __init__(self):
ProposalSubpage.__init__(self)
def run(self, prop: Proposal):
# Archive page list
self.proposal = prop
self.page = Page(site, f"{self.main_title}/{prop.full_year}")
old_text = self.page.text
award = f' {prop.award_tpl}' if prop.approved else ''
appended_text = f'{prop.first_tpl}{prop.title}|{prop.nominator}}}}}' \
f'<font color="{prop.color}">{prop.result}</font> ' \
f'el dia {prop.day} {prop.of}{prop.month} de {prop.year}.{award}\n{prop.first_tpl}'
new_text = old_text.replace(prop.first_tpl, appended_text, 1)
if self.debug:
self.show(new_text, "L'arxiu ha quedat així", f"... a l'arxiu anual <<{self.page.title(without_brackets=True)}>>")
return
self.page.text = new_text
self.page.save()
class ProposalTemplate:
"""
Plantilla que conté les darreres propostes resoltes.
"""
def __init__(self):
self.debug: bool = debug
self.title = "Plantilla:Darreres propostes resoltes"
self.page = Page(site, self.title)
self.proposal: Optional[Proposal] = None
self.pattern = re.compile(r'#\{\{ArxiuAdQ.*')
self.tpl_pattern = re.compile(r"(?P<comment><!-- màx\s+(?P<number>\d+)\s*-->)")
self.number_of_templates = 0
self.comment = ''
def set_number_of_templates(self, text):
match = self.tpl_pattern.search(text)
if match:
self.number_of_templates = int(match.group('number'))
self.comment = match.group('comment')
def run(self, prop: Proposal):
# Recent proposal list
self.proposal = prop
old_text = self.page.text
self.set_number_of_templates(old_text)
award = f' {prop.award_tpl}' if prop.approved else ''
prepended_text = f'{self.comment}\n{prop.first_tpl}{prop.title}|{prop.nominator}}}}}' \
f'<font color="{prop.color}">{prop.result}</font> ' \
f'el dia {prop.day} {prop.of}{prop.month} de {prop.full_year}.{award}'
new_text = old_text.replace(self.comment, prepended_text)
found = self.pattern.findall(new_text)
for item in found[self.number_of_templates:]:
new_text = new_text.replace(item, '')
if self.debug:
Display.show(
new_text,
"La llista ha quedat així",
f'... a la plantilla de propostes recents <<{self.page.title(without_brackets=True)}>>'
)
return
# Esborrar proposta
self.page.text = new_text
self.page.save(u"Bot esborrant proposta aprovada")
class ProposalsMainPage(ProposalSubpage):
"""
Classe filla per a la pàgina principal.
"""
def __init__(self):
ProposalSubpage.__init__(self)
def run(self, prop: Proposal):
self.page = Page(site, self.main_title)
# Remove proposal
old_text = self.page.text
search_term = f'----\n{{{{/{prop.title_without_html_entities}}}}}\n'
new_text = old_text.replace(search_term, '')
if self.debug:
self.show(
new_text,
f"La proposta per a l'article [[{prop.title}]] ha quedat així",
f"... a la pàgina principal (<<{self.page.title(without_brackets=True)}>>)"
)
return
self.page.text = new_text
self.page.save("Bot esborrant proposta aprovada")
class Article:
"""
Classe que representa un article.
"""
def __init__(self):
self.debug: bool = debug
self.proposal: Optional[Proposal] = None
self.title: Optional[str] = None
self.keep_title_case = ('terra',)
self.categories: List[str] = []
@property
def category_award(self):
return 'de qualitat' if self.proposal.award == 'qualitat' else 'bons'
@property
def template_award(self):
return 'de qualitat' if self.proposal.award == 'qualitat' else 'bo'
def fix_capital_letters(self, category):
pattern = re.compile("(?P<before>de la |del |dels |de les |de l')(?P<topic>.*)")
match = pattern.search(category)
if match:
old_topic = match.group('topic')
if old_topic in self.keep_title_case:
new_topic = old_topic.title()
category = category.replace(old_topic, new_topic)
return category
def run(self, prop: Proposal):
self.proposal = prop
self.categories = main.categories.get_categories(prop)
self.title = prop.title
bot.output(u"Fent canvis a l'article...")
category = prop.category.lower()
page = Page(site, self.title)
text = page.text
lower_cats = [cat.lower() for cat in self.categories]
prep = "d'" if category[0].lower() in "aeiouàèéíòóú" else 'de '
category = self.fix_capital_letters(category)
category = f"\n[[Categoria:Articles {self.category_award} {prep}{category}]]"
if prop.translated:
category += f"\n[[Categoria:Traduccions que són articles {self.category_award}]]"
text += f"{category}\n{{{{Article {self.template_award}}}"
if self.debug:
self.show(text)
return
def show(self, text):
Display.show(text, "L'article ha quedat així", f'... a <<{self.title}>>')
class ArticleTalk:
"""
Classe que representa la pàgina de discussió.
"""
def __init__(self):
# Actualitza l'historial d'avaluacions
self.debug: bool = debug
self.proposal: Optional[Proposal] = None
self.history_pattern = re.compile(r"\{\{Historial d'avaluacions\|.*?\}\}")
self.award_pattern = re.compile(r"\{\{Proposta distinció\|(?P<title>[\w\d]+)\|")
@property
def title(self):
return f"Discussió:{self.proposal.title}"
def run(self, prop: Proposal):
self.proposal = prop
bot.output("Fent canvis a la pàgina de discussió...")
talk_page = Page(site, self.title)
text = talk_page.text
approved = 'aprovat' if prop.approved else 'no aprovat'
text = self.history_pattern.sub(rf"{{Historial d'avaluacions|{approved}}}", text, 1)
match = self.award_pattern.search(text)
if match:
award = match.group('title')
text = self.award_pattern.sub(award, text, 1)
if self.debug:
self.show(text)
return
bot.output(u"Desant la pàgina de discussió...")
talk_page.text = text
talk_page.save(u"Bot actualitzant historial d'avalucions")
def show(self, text):
Display.show(text, "La discussió ha quedat així", f"... a <<{self.title}>>")
class GrantedArticlesCategories:
"""
Recull de categories dels AdQ i ABons.
"""
def __init__(self):
self.debug: bool = debug
self.proposal: Optional[Proposal] = None
self.fd_pattern = re.compile(
r"{\|[^|]+\|\+ \s*style=\"\"\s+\|\s+'''Categories d'Articles de Qualitat'''.*?\|(?P<categories>.*)\|}",
re.DOTALL | re.MULTILINE
)
self.gd_pattern = re.compile(r'<div style="text-align:center;">(?P<categories>.*)\|}', re.DOTALL | re.MULTILINE)
self.cat_pattern = re.compile(r"\[\[#(?P<category>[^|]+)\|[^\]]+]]")
self.good_pattern = re.compile(r"\{\{Infralliga\|(?P<category>[^}]+)}}")
self.fa_categories: List[str] = []
self.ga_categories: List[str] = []
self.dispatch()
@staticmethod
def title(award):
return f"Viquipèdia:Articles {f'de {award}' if award == 'qualitat' else 'bons/Introducció'}"
def get_categories(self, prop: Proposal):
return self.fa_categories if prop.award == 'qualitat' else self.ga_categories
def dispatch(self):
# Get featured articles categories
title = self.title('qualitat')
page = Page(site, title)
old_text = page.text
categories_text = self.fd_pattern.search(old_text)
if categories_text:
for match in self.cat_pattern.finditer(categories_text.group('categories')):
self.fa_categories.append(match.group('category'))
# Get good articles categories
title = self.title('bons')
page = Page(site, title)
old_text = page.text
categories_text = self.gd_pattern.search(old_text)
if categories_text:
for match in self.good_pattern.finditer(categories_text.group('categories')):
self.ga_categories.append(match.group('category'))
if self.debug:
if self.fa_categories:
print(self.fa_categories)
if self.ga_categories:
print(self.ga_categories)
class GrantedArticlesProject:
"""
Classe que representa la subpàgina de la categoria a la que pertany un article bo o de qualitat.
"""
def __init__(self):
self.debug: bool = debug
self.award = ''
self.category = ''
self.page: Optional[Page] = None
@property
def title(self):
return f"Viquipèdia:Articles {self.award}/{self.category}"
def sort_category(self):
pass
def run(self, prop: Proposal):
self.award = 'de qualitat' if prop.award == 'qualitat' else 'bons'
self.category = prop.category
self.page = Page(site, self.title)
self.sort_category()
class Main:
"""
Classe principal per engegar els diversos procediments.
"""
def __init__(self):
self.debug: bool = debug
self.categories: Optional[GrantedArticlesCategories] = None
self.users: Dict[str, User] = {}
def start(self, prop: Proposal):
bot.output('Arxivant proposta...')
# Categories
subpage = ArticleProposalSubpage()
subpage.run(prop)
# Llista de l'axiu
archive = ArchiveProposalSubpage()
archive.run(prop)
# Llista recentment propostes
recent_proposals = ProposalTemplate()
recent_proposals.run(prop)
# Esborrar proposta
main_page = ProposalsMainPage()
main_page.run(prop)
def collect(self):
provider = ProposalProvider()
self.categories = GrantedArticlesCategories()
for proposal in provider:
Display.begin(proposal)
self.start(proposal)
if __name__ == '__main__':
arg_parser = ArgumentParser()
arg_parser.add_argument('-D', '--debug', default=False, dest='debug', action='store_true')
args = arg_parser.parse_args()
debug = args.debug
on_win = sys.platform.startswith("win")
encoding = 'Catalan_Andorra.1252' if on_win else "ca_ES.utf8"
locale.setlocale(locale.LC_ALL, encoding)
site = Site('ca', 'wikipedia', 'ArxivaBot')
site.login()
months = Months()
main = Main()
main.collect()