diff --git a/README.md b/README.md index 28ae293..50c5c3c 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,14 @@ En este momento ya se puede hablar con el bot. ¿Qué le digo? ### Flujo admin -* `/su ` para reclamar permisos de admin, reemplazando `` por la contraseña que hayamos -elegido en la envvar `PYCAMP_BOT_MASTER_KEY` +#### Inicialización (requerida al comienzo de cada PyCamp) + +* `/su ` para reclamar permisos de admin, reemplazando `` por la contraseña que hayamos elegido en la envvar `PYCAMP_BOT_MASTER_KEY` * `/empezar_pycamp ` inicia el flujo de creación de un pycamp. Lo carga en la db, pide fecha de inicio y duración. Lo deja activo. -* `/activar_pycamp ` activa un pycamp + * `/activar_pycamp ` activa un pycamp, en caso que haga falta. + +#### Flujo de Proyectos + * `/empezar_carga_proyectos` habilita la carga de los proyectos. En este punto los pycampistas pueden cargar sus proyectos, enviandole al bot el comando `/cargar_proyecto` * `/terminar_carga_proyectos` termina carga proyectos @@ -73,10 +77,9 @@ Para generar el schedule: * `/cronogramear` te va a preguntar cuantos dias queres cronogramear y cuantos slots por dia tenes y hacer el cronograma. * `/cambiar_slot` toma un nombre de proyecto y un slot; y te cambia ese proyecto a ese slot. -Para agendar los magos: +#### Flujo de magia -1. Todos los candidatos tienen que haberse registrado con `/ser_magx` -2. Tiene que estar creado el schedule de presentaciones de proyectos (`/cronogramear`) +Para agendar los magos todos los candidatos tienen que haberse registrado con `/ser_magx` * `/agendar_magx` Asigna un mago por hora durante todo el PyCamp. * De 9 a 13 y de 14 a 19. @@ -92,3 +95,4 @@ Para agendar los magos: * `/ver_magx` Lista los magos registrados. * `/evocar_magx` llama al mago de turno para pedirle ayuda. * `/ver_agenda_magx completa` te muestra la agenda de magos del PyCamp. El parámetro `completa` es opcional, si se omite solo muestra los turnos pendientes. + diff --git a/pyproject.toml b/pyproject.toml index 259ba5d..d06e45b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ "peewee==3.17.9", "sentry-sdk==2.22.0", ] -requires-python = "==3.10.*" +requires-python = "==3.11.*" authors = [ {name = "Pyar", email = "pyar@pyar.com"}, ] diff --git a/src/pycamp_bot/commands/help_msg.py b/src/pycamp_bot/commands/help_msg.py index b5f1b2f..d768b5d 100644 --- a/src/pycamp_bot/commands/help_msg.py +++ b/src/pycamp_bot/commands/help_msg.py @@ -24,24 +24,26 @@ ''' HELP_MESSAGE = ''' -Este bot facilita la carga, administración y procesamiento de \ +Este bot facilita la carga, administración y procesamiento de magues, \ proyectos y votos durante el PyCamp El proceso se divide en 3 etapas: -*Primera etapa*: Lxs responsables de los proyectos cargan sus proyectos \ +*Primera etapa*: Iniciar el PyCamp\\. Algún admin del Bot + +*Segunda etapa*: Lxs responsables de los proyectos cargan sus proyectos \ mediante el comando */cargar\\_proyecto*\\. Solo un responsable carga el \ proyecto, y luego si hay otrxs responsables adicionales, pueden \ agregarse con el comando */ownear*\\. -*Segunda etapa*: Mediante el comando */elegir\\_proyectos* todxs lxs participantes \ +*Tercera etapa*: Mediante el comando */elegir\\_proyectos* todxs lxs participantes \ seleccionan los proyectos que se expongan\\. Esto se puede hacer a medida que \ se expone, o al haber finalizado todas las exposiciones\\. Si no se está \ segurx de un proyecto, conviene no seleccionar nada, ya que luego podés \ volver a ejecutar el comando y darle que si aquellas cosas que no tocaste\\. NO \ SE PUEDE CAMBIAR TU RESPUESTA UNA VEZ HECHO\\. -*Tercera etapa*: Lxs admins mergean los proyectos que se haya decidido \ +*Cuarta etapa*: Lxs admins mergean los proyectos que se haya decidido \ mergear durante las exposiciones \\(Por tematica similar, u otros \ motivos\\), y luego se procesan los datos para obtener el cronograma \ final\\. @@ -53,6 +55,7 @@ Pycamp: /activar\\_pycamp \\(pycamp\\): Setea un pycamp como activo \\(si ya hay uno activo lo \ desactiva\\)\\. + /empezar\\_carga\\_proyectos: Habilita la carga de proyectos en el pycamp activo\\. /terminar\\_carga\\_proyectos: Deshabilita la carga de proyectos en el pycamp activo\\. /empezar\\_seleccion\\_proyectos: Habilita la seleccion sobre los proyectos del pycamp activo\\. @@ -66,6 +69,14 @@ /cambiar\\_slot: Toma el nombre de un proyecto y el nuevo slot \ y lo cambia en el cronograma\\. +**Gestión de magxs** + +/ser\\_magx Tienen que ejecutar los candidatos, al inicio del PyCamp\\. +/agendar\\_magx Genera una agenda de magxs para todo el evento\\. +/ver\\_agenda\\_magx Para conocer la agenda magos de todo el evento\\. +/ver\\_magx Para conocer el magx actual\\. +/evocar\\_magx Para llamar al mago actual\\. + Pycampista: /degradar \\(username\\): Le saca los permisos de admin a un usuario\\. ''' + user_commands_help diff --git a/src/pycamp_bot/commands/wizard.py b/src/pycamp_bot/commands/wizard.py index 76e856f..e8e9c17 100644 --- a/src/pycamp_bot/commands/wizard.py +++ b/src/pycamp_bot/commands/wizard.py @@ -8,7 +8,7 @@ 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.utils import escape_markdown +from pycamp_bot.utils import escape_markdown, active_pycamp_needed LUNCH_TIME_START_HOUR = 13 @@ -80,7 +80,7 @@ def define_wizards_schedule(pycamp): """ all_wizards = pycamp.get_wizards() - if all_wizards.count() == 0: + if len(all_wizards) == 0: return {} wizard_per_slot = {} @@ -103,19 +103,12 @@ def define_wizards_schedule(pycamp): return wizard_per_slot -async def become_wizard(update, context): - current_wizards = Pycampista.select().where(Pycampista.wizard is True) - - for w in current_wizards: - w.current = False - w.save() - +@active_pycamp_needed +async def become_wizard(update, context, pycamp=None): username = update.message.from_user.username chat_id = update.message.chat_id - user = Pycampista.get_or_create(username=username, chat_id=chat_id)[0] - user.wizard = True - user.save() + pycamp.add_wizard(username, chat_id) await context.bot.send_message( chat_id=update.message.chat_id, @@ -123,8 +116,8 @@ async def become_wizard(update, context): ) -async def list_wizards(update, context): - _, pycamp = get_active_pycamp() +@active_pycamp_needed +async def list_wizards(update, context, pycamp=None): msg = "" for i, wizard in enumerate(pycamp.get_wizards()): msg += "{}) @{}\n".format(i+1, wizard.username) @@ -137,8 +130,8 @@ async def list_wizards(update, context): logger.exception("Coulnd't deliver the Wizards list to {}".format(update.message.from_user.username)) -async def summon_wizard(update, context): - _, pycamp = get_active_pycamp() +@active_pycamp_needed +async def summon_wizard(update, context, pycamp=None): wizard = pycamp.get_current_wizard() if wizard is None: await context.bot.send_message( @@ -158,14 +151,20 @@ async def summon_wizard(update, context): text="Checkeá tu cabeza: si no ténes el sombrero de magx ¡deberías!\n(soltá la compu)" ) else: - await context.bot.send_message( - chat_id=wizard.chat_id, - text="PING PING PING MAGX! @{} te necesita!".format(username) - ) - await context.bot.send_message( - chat_id=update.message.chat_id, + try: + await context.bot.send_message( + chat_id=wizard.chat_id, + text="PING PING PING MAGX! @{} te necesita!".format(username) + ) text="Tu magx asignadx es: @{}".format(wizard.username) - ) + except BadRequest: + text="No se pudo notificar al magx asignadx: @{} Andá a buscarlo...".format(wizard.username) + logger.warn("Coulnd't notify the wizard {}".format(wizard.username)) + finally: + await context.bot.send_message( + chat_id=update.message.chat_id, + text=text + ) async def notify_scheduled_slots_to_wizard(update, context, pycamp, wizard, agenda): per_day = defaultdict(list) @@ -220,9 +219,8 @@ def persist_wizards_schedule_in_db(pycamp): @admin_needed -async def schedule_wizards(update, context): - _, pycamp = get_active_pycamp() - +@active_pycamp_needed +async def schedule_wizards(update, context, pycamp=None): n = pycamp.clear_wizards_schedule() logger.info("Deleted wizards schedule ({} records)".format(n)) @@ -281,7 +279,8 @@ def aux_resolve_show_all(message): return show_all -async def show_wizards_schedule(update, context): +@active_pycamp_needed +async def show_wizards_schedule(update, context, pycamp=None): try: show_all = aux_resolve_show_all(update.message) except ValueError: @@ -290,8 +289,6 @@ async def show_wizards_schedule(update, context): text="El comando solo acepta un parámetro (opcional): 'completa'. ¿Probás de nuevo?", ) return - - _, pycamp = get_active_pycamp() agenda = WizardAtPycamp.select().where(WizardAtPycamp.pycamp == pycamp) if not show_all: diff --git a/src/pycamp_bot/models.py b/src/pycamp_bot/models.py index 0ad482c..935c770 100644 --- a/src/pycamp_bot/models.py +++ b/src/pycamp_bot/models.py @@ -1,6 +1,10 @@ import peewee as pw from datetime import datetime, timedelta +import datetime +from zoneinfo import ZoneInfo +from pycamp_bot.logger import logger + from random import choice @@ -89,13 +93,25 @@ def set_as_only_active(self): self.active = True self.save() + def add_wizard(self, username, chat_id): + pycampista = Pycampista.get_or_create(username=username, chat_id=chat_id)[0] + pycampista.wizard = True + pycampista.save() + PycampistaAtPycamp.get_or_create(pycamp=self, pycampista=pycampista) + return pycampista + def get_wizards(self): - return Pycampista.select().where(Pycampista.wizard == 1) + pac = PycampistaAtPycamp.select().join(Pycampista).where( + (PycampistaAtPycamp.pycamp == self) & + (PycampistaAtPycamp.pycampista.wizard == True) + ) + return [p.pycampista for p in pac] def get_current_wizard(self): """Return the Pycampista instance that's the currently scheduled wizard.""" - now = datetime.now() - current_wizards = WizardAtPycamp.select().where( + now = datetime.datetime.now(ZoneInfo("America/Argentina/Cordoba")) + logger.info("Request wizard at user time: %s", str(now)) + current_wizards = WizardAtPycamp.select().where( (WizardAtPycamp.pycamp == self) & (WizardAtPycamp.init <= now) & (WizardAtPycamp.end > now) diff --git a/src/pycamp_bot/utils.py b/src/pycamp_bot/utils.py index 161f064..7c87902 100644 --- a/src/pycamp_bot/utils.py +++ b/src/pycamp_bot/utils.py @@ -1,4 +1,6 @@ from pycamp_bot.models import Pycamp +from pycamp_bot.logger import logger + def escape_markdown(string): # See: https://core.telegram.org/bots/api#markdownv2-style @@ -30,3 +32,21 @@ def get_slot_weekday_name(slot_day_code): day_name = ISO_WEEKDAY_NAMES[pycamp_start_weekday + offset] return day_name +def active_pycamp_needed(f): + from pycamp_bot.commands.manage_pycamp import get_active_pycamp + async def wrap(*args, **kargs): + update, context = args + + _, pycamp = get_active_pycamp() + if pycamp is None: + msg = "🔥 %s: This operation (%s) needs an active PyCamp. Talk to an admin." % ( + update.message.from_user.username, str(f.__name__) + ) + await context.bot.send_message( + chat_id=update.message.chat_id, + text=msg) + logger.warning(msg) + return + + return await f(*args, pycamp=pycamp) + return wrap diff --git a/test/conftest.py b/test/conftest.py index e132919..b57cf62 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,13 +5,13 @@ from peewee import SqliteDatabase from telegram import Bot -from pycamp_bot.models import Pycampista, Slot, Pycamp, WizardAtPycamp +from pycamp_bot.models import Pycampista, Slot, Pycamp, WizardAtPycamp, PycampistaAtPycamp # use an in-memory SQLite for tests. test_db = SqliteDatabase(':memory:') -MODELS = [Pycampista, Slot, Pycamp, WizardAtPycamp] +MODELS = [Pycampista, Slot, Pycamp, WizardAtPycamp, PycampistaAtPycamp] def use_test_database(fn): diff --git a/test/test_pycamp_model.py b/test/test_pycamp_model.py index edd387c..56248de 100644 --- a/test/test_pycamp_model.py +++ b/test/test_pycamp_model.py @@ -42,7 +42,7 @@ def test_returns_correct_wizard_within_its_turno(self): init=datetime(2024,6,20), end=datetime(2024,6,23), ) - pycamper = Pycampista.create(username="pepe", wizard=True) + pycamper = p.add_wizard("pepe", 123) wizard.persist_wizards_schedule_in_db(p) assert p.get_current_wizard() == pycamper diff --git a/test/test_wizard.py b/test/test_wizard.py index fae1774..5b020be 100644 --- a/test/test_wizard.py +++ b/test/test_wizard.py @@ -22,7 +22,7 @@ def teardown_module(module): # database here. But for tests this is probably not necessary. -class TestWizardScheduleSlots: +class BaseForOtherWizardsTests: def init_pycamp(self): self.pycamp = Pycamp.create( @@ -31,6 +31,8 @@ def init_pycamp(self): end=datetime(2024,6,24), ) + +class TestWizardScheduleSlots(BaseForOtherWizardsTests): @use_test_database def test_correct_number_of_slots_in_one_day(self): p = Pycamp.create( @@ -95,14 +97,7 @@ def test_no_slot_after_last_day_lunch(self): assert start >= lunch_time_end -class TestDefineWizardsSchedule: - - def init_pycamp(self): - self.pycamp = Pycamp.create( - headquarters="Narnia", - init=datetime(2024,6,20), - end=datetime(2024,6,24), - ) +class TestDefineWizardsSchedule(BaseForOtherWizardsTests): # If no wizards, returns {} @use_test_database @@ -164,3 +159,41 @@ def test_all_slots_are_signed_a_wizard(self): assert all( (isinstance(s, Pycampista) and s.wizard) for s in sched.values() ) + +class TestListWizards(BaseForOtherWizardsTests): + + @use_test_database + def test_wizard_registration(self): + self.init_pycamp() + w = self.pycamp.add_wizard("Gandalf", 123) + wizards = self.pycamp.get_wizards() + assert len(wizards) == 1 + assert w.username == wizards[0].username + + + @use_test_database + def test_wizard_registration_works_in_one_pycamp_only(self): + self.init_pycamp() + self.pycamp.add_wizard("Gandalf", 123) + + other_pycamp = Pycamp.create( + headquarters="Mordor", + init=datetime(2025,3,22), + end=datetime(2025,3,24), + ) + w = other_pycamp.add_wizard("Merlin", 456) + #import ipdb; ipdb.set_trace() + results = other_pycamp.get_wizards() + assert len(results) == 1 + assert w.username == results[0].username + + + # @use_test_database + # def test_no_active_pycamp_then_fail(self): + # self.init_pycamp() + # agregarle un mago + + # creo OTRO Pycamp + # agregarle otro mago + + # pedir listado: check está el OTRO mago solamente \ No newline at end of file