Skip to content
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,14 @@ En este momento ya se puede hablar con el bot. ¿Qué le digo?

### Flujo admin

* `/su <password>` para reclamar permisos de admin, reemplazando `<password>` por la contraseña que hayamos
elegido en la envvar `PYCAMP_BOT_MASTER_KEY`
#### Inicialización (requerida al comienzo de cada PyCamp)

* `/su <password>` para reclamar permisos de admin, reemplazando `<password>` por la contraseña que hayamos elegido en la envvar `PYCAMP_BOT_MASTER_KEY`
* `/empezar_pycamp <pycamp_name>` 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 <pycamp_name>` activa un pycamp
* `/activar_pycamp <pycamp_name>` 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
Expand All @@ -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.
Expand All @@ -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.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]"},
]
Expand Down
19 changes: 15 additions & 4 deletions src/pycamp_bot/commands/help_msg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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\\.
Expand All @@ -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\\.
Expand All @@ -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
Expand Down
55 changes: 26 additions & 29 deletions src/pycamp_bot/commands/wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {}
Expand All @@ -103,28 +103,21 @@ 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,
text="¡Felicidades! Has sido registrado como magx."
)


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)
Expand All @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
22 changes: 19 additions & 3 deletions src/pycamp_bot/models.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions src/pycamp_bot/utils.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion test/test_pycamp_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 42 additions & 9 deletions test/test_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading