diff --git a/src/pycamp_bot/commands/announcements.py b/src/pycamp_bot/commands/announcements.py index 69a0236..c13e2c0 100644 --- a/src/pycamp_bot/commands/announcements.py +++ b/src/pycamp_bot/commands/announcements.py @@ -50,7 +50,7 @@ async def announce(update: Update, context: CallbackContext) -> str: chat_id=update.message.chat_id, text=ERROR_MESSAGES["no_admin"], ) - logger.warn(f"Pycampista {state.username} no contiene proyectos creados.") + logger.warning(f"Pycampista {state.username} no contiene proyectos creados.") return ConversationHandler.END else: state.projects = Project.select() diff --git a/src/pycamp_bot/commands/projects.py b/src/pycamp_bot/commands/projects.py index 3d80c4b..d59527e 100644 --- a/src/pycamp_bot/commands/projects.py +++ b/src/pycamp_bot/commands/projects.py @@ -2,6 +2,7 @@ import textwrap import peewee +from peewee import JOIN from telegram import InlineKeyboardButton, InlineKeyboardMarkup, LinkPreviewOptions from telegram.ext import CallbackQueryHandler, CommandHandler, ConversationHandler, MessageHandler, filters from pycamp_bot.models import Pycampista, Project, Slot, Vote @@ -528,9 +529,9 @@ async def show_my_projects(update, context): ) votes = ( Vote - .select(Project, Slot) + .select(Vote, Project, Slot) .join(Project) - .join(Slot) + .join(Slot, join_type=JOIN.LEFT_OUTER) .where( (Vote.pycampista == user) & Vote.interest @@ -544,17 +545,28 @@ async def show_my_projects(update, context): prev_slot_day_code = None for vote in votes: - slot_day_code = vote.project.slot.code[0] - slot_day_name = get_slot_weekday_name(slot_day_code) + slot = vote.project.slot + if slot is None: + slot_day_code = None + slot_day_name = "Sin asignar" + else: + slot_day_code = slot.code[0] + slot_day_name = get_slot_weekday_name(slot_day_code) if slot_day_code != prev_slot_day_code: text_chunks.append(f'*{slot_day_name}*') - project_lines = [ - f'{vote.project.slot.start}:00', - escape_markdown(vote.project.name), - f'Owner: @{escape_markdown(vote.project.owner.username)}', - ] + if slot is None: + project_lines = [ + escape_markdown(vote.project.name), + f'Owner: @{escape_markdown(vote.project.owner.username)}', + ] + else: + project_lines = [ + f'{slot.start}:00', + escape_markdown(vote.project.name), + f'Owner: @{escape_markdown(vote.project.owner.username)}', + ] text_chunks.append('\n'.join(project_lines)) diff --git a/src/pycamp_bot/commands/schedule.py b/src/pycamp_bot/commands/schedule.py index 19c9c51..1ac1551 100644 --- a/src/pycamp_bot/commands/schedule.py +++ b/src/pycamp_bot/commands/schedule.py @@ -117,7 +117,7 @@ async def create_slot(update, context): chat_id=update.message.chat_id, text="Genial! Slots Asignados" ) - make_schedule(update, context) + await make_schedule(update, context) return ConversationHandler.END diff --git a/src/pycamp_bot/commands/voting.py b/src/pycamp_bot/commands/voting.py index db8c143..f0b6f54 100644 --- a/src/pycamp_bot/commands/voting.py +++ b/src/pycamp_bot/commands/voting.py @@ -47,10 +47,10 @@ async def start_voting(update, context): async def button(update, context): '''Save user vote in the database''' query = update.callback_query - username = query.message['chat']['username'] + username = query.from_user.username chat_id = query.message.chat_id user = Pycampista.get_or_create(username=username, chat_id=chat_id)[0] - project_name = query.message['text'] + project_name = query.message.text # Get project from the database project = Project.get(Project.name == project_name) @@ -95,7 +95,11 @@ async def vote(update, context): # if there is not project in the database, create a new project if not Project.select().exists(): - Project.create(name='PROYECTO DE PRUEBA') + user = Pycampista.get_or_create( + username=update.message.from_user.username, + chat_id=str(update.message.chat_id), + )[0] + Project.create(name='PROYECTO DE PRUEBA', owner=user) # ask user for each project in the database for project in Project.select(): @@ -121,6 +125,8 @@ async def end_voting(update, context): await update.message.reply_text("Selección cerrada") await msg_to_active_pycamp_chat(context.bot, "La selección de proyectos ha finalizado.") + +@admin_needed async def vote_count(update, context): votes = [vote.pycampista_id for vote in Vote.select()] vote_count = len(set(votes)) diff --git a/src/pycamp_bot/commands/wizard.py b/src/pycamp_bot/commands/wizard.py index e8e9c17..1e0b276 100644 --- a/src/pycamp_bot/commands/wizard.py +++ b/src/pycamp_bot/commands/wizard.py @@ -1,13 +1,16 @@ -import random from collections import defaultdict from datetime import datetime, timedelta from itertools import cycle -from telegram.ext import CommandHandler +import random +from zoneinfo import ZoneInfo + from telegram.error import BadRequest -from pycamp_bot.models import Pycampista, WizardAtPycamp +from telegram.ext import CommandHandler + from pycamp_bot.commands.auth import admin_needed from pycamp_bot.commands.manage_pycamp import get_active_pycamp from pycamp_bot.logger import logger +from pycamp_bot.models import Pycampista, WizardAtPycamp from pycamp_bot.utils import escape_markdown, active_pycamp_needed @@ -240,7 +243,7 @@ async def schedule_wizards(update, context, pycamp=None): parse_mode="MarkdownV2" ) except BadRequest as e: - m = "Coulnd't return the Wizards list to the admin. ".format(update.message.from_user.username) + m = "Couldn't return the Wizards list to the admin ({}).".format(update.message.from_user.username) if len(msg) >= MSG_MAX_LEN: m += "The message is too long. Check the data in the DB ;-)" logger.exception(m) @@ -264,44 +267,65 @@ def format_wizards_schedule(agenda): ) return msg -def aux_resolve_show_all(message): - show_all = False - parameters = message.text.strip().split(' ', 1) - if len(parameters) == 2: - flag = parameters[1].strip().lower() - show_all = (flag == "completa") # Once here, the only parameter must be valid - if not show_all: - # The parameter was something else... +def aux_resolve_show_all(context): + """Usa context.args: sin args o 'completa' = agenda completa; 'futuros' = solo turnos futuros.""" + show_all = True # por defecto mostrar toda la agenda (evita problemas de timezone en containers) + args = (context.args or []) + if len(args) == 1: + flag = args[0].strip().lower() + if flag == "completa": + show_all = True + elif flag == "futuros": + show_all = False + else: raise ValueError("Wrong parameter") - elif len(parameters) > 2: - # Too many parameters... - raise ValueError("Wrong parameter") + elif len(args) > 1: + raise ValueError("Too many parameters") return show_all @active_pycamp_needed async def show_wizards_schedule(update, context, pycamp=None): try: - show_all = aux_resolve_show_all(update.message) + show_all = aux_resolve_show_all(context) except ValueError: await context.bot.send_message( chat_id=update.message.chat_id, - text="El comando solo acepta un parámetro (opcional): 'completa'. ¿Probás de nuevo?", + text="El comando acepta un parámetro opcional: 'completa' (ver todo) o 'futuros' (solo turnos por venir). ¿Probás de nuevo?", ) return agenda = WizardAtPycamp.select().where(WizardAtPycamp.pycamp == pycamp) if not show_all: - agenda = agenda.where(WizardAtPycamp.end > datetime.now()) + # Solo futuros: comparar con hora Argentina (los slots en DB son hora local Córdoba) + now_argentina = datetime.now(ZoneInfo("America/Argentina/Cordoba")).replace(tzinfo=None) + agenda = agenda.where(WizardAtPycamp.end > now_argentina) agenda = agenda.order_by(WizardAtPycamp.init) - msg = format_wizards_schedule(agenda) - - await context.bot.send_message( - chat_id=update.message.chat_id, - text=msg, - parse_mode="MarkdownV2" - ) + count = agenda.count() + if count == 0: + if show_all: + msg = ( + "Agenda de magxs:\n\n" + "No hay turnos cargados. Un admin debe ejecutar /agendar_magx " + "después de que magxs se anoten con /ser_magx." + ) + else: + msg = ( + "Agenda de magxs:\n\n" + "No hay turnos futuros. Probá con /ver_agenda_magx completa para ver toda la agenda." + ) + await context.bot.send_message( + chat_id=update.message.chat_id, + text=msg, + ) + else: + msg = format_wizards_schedule(agenda) + await context.bot.send_message( + chat_id=update.message.chat_id, + text=msg, + parse_mode="MarkdownV2", + ) logger.debug("Wizards schedule delivered to {}".format(update.message.from_user.username)) diff --git a/src/pycamp_bot/models.py b/src/pycamp_bot/models.py index 935c770..5ddaf99 100644 --- a/src/pycamp_bot/models.py +++ b/src/pycamp_bot/models.py @@ -86,10 +86,11 @@ def __str__(self): return rv_str def set_as_only_active(self): - active = Pycamp.select().where(Pycamp.active) + active = list(Pycamp.select().where(Pycamp.active)) for p in active: p.active = False - Pycamp.bulk_update(active, fields=[Pycamp.active]) + if active: + Pycamp.bulk_update(active, fields=[Pycamp.active]) self.active = True self.save() diff --git a/src/pycamp_bot/scheduler/schedule_calculator.py b/src/pycamp_bot/scheduler/schedule_calculator.py index bae190f..0013570 100644 --- a/src/pycamp_bot/scheduler/schedule_calculator.py +++ b/src/pycamp_bot/scheduler/schedule_calculator.py @@ -128,7 +128,8 @@ def value(self, state): # were at the begining vote_quantity = sum([len(self.data.projects[project].votes) for project in slot_projects]) - most_voted_cost += (slot_number * vote_quantity) / self.total_participants + denom = max(1, self.total_participants) + most_voted_cost += (slot_number * vote_quantity) / denom for project, slot in state: project_data = self.data.projects[project] @@ -221,6 +222,8 @@ def hill_climbing(problem, initial_state): while True: neighboors = [(n, problem.value(n)) for n in problem.neighboors(current_state)] + if not neighboors: + return current_state best_neighbour, best_value = max(neighboors, key=itemgetter(1)) if best_value > current_value: