Skip to content

Commit ab895f2

Browse files
authored
Merge pull request #124 from PyAr/wizards-management-resilience
Wizards management resilience
2 parents 79e3991 + a1d674a commit ab895f2

File tree

9 files changed

+136
-55
lines changed

9 files changed

+136
-55
lines changed

README.md

+10-6
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,14 @@ En este momento ya se puede hablar con el bot. ¿Qué le digo?
5858

5959
### Flujo admin
6060

61-
* `/su <password>` para reclamar permisos de admin, reemplazando `<password>` por la contraseña que hayamos
62-
elegido en la envvar `PYCAMP_BOT_MASTER_KEY`
61+
#### Inicialización (requerida al comienzo de cada PyCamp)
62+
63+
* `/su <password>` para reclamar permisos de admin, reemplazando `<password>` por la contraseña que hayamos elegido en la envvar `PYCAMP_BOT_MASTER_KEY`
6364
* `/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.
64-
* `/activar_pycamp <pycamp_name>` activa un pycamp
65+
* `/activar_pycamp <pycamp_name>` activa un pycamp, en caso que haga falta.
66+
67+
#### Flujo de Proyectos
68+
6569
* `/empezar_carga_proyectos` habilita la carga de los proyectos. En este punto los pycampistas pueden cargar sus proyectos,
6670
enviandole al bot el comando `/cargar_proyecto`
6771
* `/terminar_carga_proyectos` termina carga proyectos
@@ -73,10 +77,9 @@ Para generar el schedule:
7377
* `/cronogramear` te va a preguntar cuantos dias queres cronogramear y cuantos slots por dia tenes y hacer el cronograma.
7478
* `/cambiar_slot` toma un nombre de proyecto y un slot; y te cambia ese proyecto a ese slot.
7579

76-
Para agendar los magos:
80+
#### Flujo de magia
7781

78-
1. Todos los candidatos tienen que haberse registrado con `/ser_magx`
79-
2. Tiene que estar creado el schedule de presentaciones de proyectos (`/cronogramear`)
82+
Para agendar los magos todos los candidatos tienen que haberse registrado con `/ser_magx`
8083

8184
* `/agendar_magx` Asigna un mago por hora durante todo el PyCamp.
8285
* De 9 a 13 y de 14 a 19.
@@ -92,3 +95,4 @@ Para agendar los magos:
9295
* `/ver_magx` Lista los magos registrados.
9396
* `/evocar_magx` llama al mago de turno para pedirle ayuda.
9497
* `/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.
98+

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ dependencies = [
1111
"peewee==3.17.9",
1212
"sentry-sdk==2.22.0",
1313
]
14-
requires-python = "==3.10.*"
14+
requires-python = "==3.11.*"
1515
authors = [
1616
{name = "Pyar", email = "[email protected]"},
1717
]

src/pycamp_bot/commands/help_msg.py

+15-4
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,26 @@
2424
'''
2525

2626
HELP_MESSAGE = '''
27-
Este bot facilita la carga, administración y procesamiento de \
27+
Este bot facilita la carga, administración y procesamiento de magues, \
2828
proyectos y votos durante el PyCamp
2929
3030
El proceso se divide en 3 etapas:
3131
32-
*Primera etapa*: Lxs responsables de los proyectos cargan sus proyectos \
32+
*Primera etapa*: Iniciar el PyCamp\\. Algún admin del Bot
33+
34+
*Segunda etapa*: Lxs responsables de los proyectos cargan sus proyectos \
3335
mediante el comando */cargar\\_proyecto*\\. Solo un responsable carga el \
3436
proyecto, y luego si hay otrxs responsables adicionales, pueden \
3537
agregarse con el comando */ownear*\\.
3638
37-
*Segunda etapa*: Mediante el comando */elegir\\_proyectos* todxs lxs participantes \
39+
*Tercera etapa*: Mediante el comando */elegir\\_proyectos* todxs lxs participantes \
3840
seleccionan los proyectos que se expongan\\. Esto se puede hacer a medida que \
3941
se expone, o al haber finalizado todas las exposiciones\\. Si no se está \
4042
segurx de un proyecto, conviene no seleccionar nada, ya que luego podés \
4143
volver a ejecutar el comando y darle que si aquellas cosas que no tocaste\\. NO \
4244
SE PUEDE CAMBIAR TU RESPUESTA UNA VEZ HECHO\\.
4345
44-
*Tercera etapa*: Lxs admins mergean los proyectos que se haya decidido \
46+
*Cuarta etapa*: Lxs admins mergean los proyectos que se haya decidido \
4547
mergear durante las exposiciones \\(Por tematica similar, u otros \
4648
motivos\\), y luego se procesan los datos para obtener el cronograma \
4749
final\\.
@@ -53,6 +55,7 @@
5355
Pycamp:
5456
/activar\\_pycamp \\(pycamp\\): Setea un pycamp como activo \\(si ya hay uno activo lo \
5557
desactiva\\)\\.
58+
5659
/empezar\\_carga\\_proyectos: Habilita la carga de proyectos en el pycamp activo\\.
5760
/terminar\\_carga\\_proyectos: Deshabilita la carga de proyectos en el pycamp activo\\.
5861
/empezar\\_seleccion\\_proyectos: Habilita la seleccion sobre los proyectos del pycamp activo\\.
@@ -66,6 +69,14 @@
6669
/cambiar\\_slot: Toma el nombre de un proyecto y el nuevo slot \
6770
y lo cambia en el cronograma\\.
6871
72+
**Gestión de magxs**
73+
74+
/ser\\_magx Tienen que ejecutar los candidatos, al inicio del PyCamp\\.
75+
/agendar\\_magx Genera una agenda de magxs para todo el evento\\.
76+
/ver\\_agenda\\_magx Para conocer la agenda magos de todo el evento\\.
77+
/ver\\_magx Para conocer el magx actual\\.
78+
/evocar\\_magx Para llamar al mago actual\\.
79+
6980
Pycampista:
7081
/degradar \\(username\\): Le saca los permisos de admin a un usuario\\.
7182
''' + user_commands_help

src/pycamp_bot/commands/wizard.py

+26-29
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pycamp_bot.commands.auth import admin_needed
99
from pycamp_bot.commands.manage_pycamp import get_active_pycamp
1010
from pycamp_bot.logger import logger
11-
from pycamp_bot.utils import escape_markdown
11+
from pycamp_bot.utils import escape_markdown, active_pycamp_needed
1212

1313

1414
LUNCH_TIME_START_HOUR = 13
@@ -80,7 +80,7 @@ def define_wizards_schedule(pycamp):
8080
8181
"""
8282
all_wizards = pycamp.get_wizards()
83-
if all_wizards.count() == 0:
83+
if len(all_wizards) == 0:
8484
return {}
8585

8686
wizard_per_slot = {}
@@ -103,28 +103,21 @@ def define_wizards_schedule(pycamp):
103103
return wizard_per_slot
104104

105105

106-
async def become_wizard(update, context):
107-
current_wizards = Pycampista.select().where(Pycampista.wizard is True)
108-
109-
for w in current_wizards:
110-
w.current = False
111-
w.save()
112-
106+
@active_pycamp_needed
107+
async def become_wizard(update, context, pycamp=None):
113108
username = update.message.from_user.username
114109
chat_id = update.message.chat_id
115110

116-
user = Pycampista.get_or_create(username=username, chat_id=chat_id)[0]
117-
user.wizard = True
118-
user.save()
111+
pycamp.add_wizard(username, chat_id)
119112

120113
await context.bot.send_message(
121114
chat_id=update.message.chat_id,
122115
text="¡Felicidades! Has sido registrado como magx."
123116
)
124117

125118

126-
async def list_wizards(update, context):
127-
_, pycamp = get_active_pycamp()
119+
@active_pycamp_needed
120+
async def list_wizards(update, context, pycamp=None):
128121
msg = ""
129122
for i, wizard in enumerate(pycamp.get_wizards()):
130123
msg += "{}) @{}\n".format(i+1, wizard.username)
@@ -137,8 +130,8 @@ async def list_wizards(update, context):
137130
logger.exception("Coulnd't deliver the Wizards list to {}".format(update.message.from_user.username))
138131

139132

140-
async def summon_wizard(update, context):
141-
_, pycamp = get_active_pycamp()
133+
@active_pycamp_needed
134+
async def summon_wizard(update, context, pycamp=None):
142135
wizard = pycamp.get_current_wizard()
143136
if wizard is None:
144137
await context.bot.send_message(
@@ -158,14 +151,20 @@ async def summon_wizard(update, context):
158151
text="Checkeá tu cabeza: si no ténes el sombrero de magx ¡deberías!\n(soltá la compu)"
159152
)
160153
else:
161-
await context.bot.send_message(
162-
chat_id=wizard.chat_id,
163-
text="PING PING PING MAGX! @{} te necesita!".format(username)
164-
)
165-
await context.bot.send_message(
166-
chat_id=update.message.chat_id,
154+
try:
155+
await context.bot.send_message(
156+
chat_id=wizard.chat_id,
157+
text="PING PING PING MAGX! @{} te necesita!".format(username)
158+
)
167159
text="Tu magx asignadx es: @{}".format(wizard.username)
168-
)
160+
except BadRequest:
161+
text="No se pudo notificar al magx asignadx: @{} Andá a buscarlo...".format(wizard.username)
162+
logger.warn("Coulnd't notify the wizard {}".format(wizard.username))
163+
finally:
164+
await context.bot.send_message(
165+
chat_id=update.message.chat_id,
166+
text=text
167+
)
169168

170169
async def notify_scheduled_slots_to_wizard(update, context, pycamp, wizard, agenda):
171170
per_day = defaultdict(list)
@@ -220,9 +219,8 @@ def persist_wizards_schedule_in_db(pycamp):
220219

221220

222221
@admin_needed
223-
async def schedule_wizards(update, context):
224-
_, pycamp = get_active_pycamp()
225-
222+
@active_pycamp_needed
223+
async def schedule_wizards(update, context, pycamp=None):
226224
n = pycamp.clear_wizards_schedule()
227225
logger.info("Deleted wizards schedule ({} records)".format(n))
228226

@@ -281,7 +279,8 @@ def aux_resolve_show_all(message):
281279
return show_all
282280

283281

284-
async def show_wizards_schedule(update, context):
282+
@active_pycamp_needed
283+
async def show_wizards_schedule(update, context, pycamp=None):
285284
try:
286285
show_all = aux_resolve_show_all(update.message)
287286
except ValueError:
@@ -290,8 +289,6 @@ async def show_wizards_schedule(update, context):
290289
text="El comando solo acepta un parámetro (opcional): 'completa'. ¿Probás de nuevo?",
291290
)
292291
return
293-
294-
_, pycamp = get_active_pycamp()
295292

296293
agenda = WizardAtPycamp.select().where(WizardAtPycamp.pycamp == pycamp)
297294
if not show_all:

src/pycamp_bot/models.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import peewee as pw
22

33
from datetime import datetime, timedelta
4+
import datetime
5+
from zoneinfo import ZoneInfo
6+
from pycamp_bot.logger import logger
7+
48
from random import choice
59

610

@@ -89,13 +93,25 @@ def set_as_only_active(self):
8993
self.active = True
9094
self.save()
9195

96+
def add_wizard(self, username, chat_id):
97+
pycampista = Pycampista.get_or_create(username=username, chat_id=chat_id)[0]
98+
pycampista.wizard = True
99+
pycampista.save()
100+
PycampistaAtPycamp.get_or_create(pycamp=self, pycampista=pycampista)
101+
return pycampista
102+
92103
def get_wizards(self):
93-
return Pycampista.select().where(Pycampista.wizard == 1)
104+
pac = PycampistaAtPycamp.select().join(Pycampista).where(
105+
(PycampistaAtPycamp.pycamp == self) &
106+
(PycampistaAtPycamp.pycampista.wizard == True)
107+
)
108+
return [p.pycampista for p in pac]
94109

95110
def get_current_wizard(self):
96111
"""Return the Pycampista instance that's the currently scheduled wizard."""
97-
now = datetime.now()
98-
current_wizards = WizardAtPycamp.select().where(
112+
now = datetime.datetime.now(ZoneInfo("America/Argentina/Cordoba"))
113+
logger.info("Request wizard at user time: %s", str(now))
114+
current_wizards = WizardAtPycamp.select().where(
99115
(WizardAtPycamp.pycamp == self) &
100116
(WizardAtPycamp.init <= now) &
101117
(WizardAtPycamp.end > now)

src/pycamp_bot/utils.py

+20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from pycamp_bot.models import Pycamp
2+
from pycamp_bot.logger import logger
3+
24

35
def escape_markdown(string):
46
# See: https://core.telegram.org/bots/api#markdownv2-style
@@ -30,3 +32,21 @@ def get_slot_weekday_name(slot_day_code):
3032
day_name = ISO_WEEKDAY_NAMES[pycamp_start_weekday + offset]
3133

3234
return day_name
35+
def active_pycamp_needed(f):
36+
from pycamp_bot.commands.manage_pycamp import get_active_pycamp
37+
async def wrap(*args, **kargs):
38+
update, context = args
39+
40+
_, pycamp = get_active_pycamp()
41+
if pycamp is None:
42+
msg = "🔥 %s: This operation (%s) needs an active PyCamp. Talk to an admin." % (
43+
update.message.from_user.username, str(f.__name__)
44+
)
45+
await context.bot.send_message(
46+
chat_id=update.message.chat_id,
47+
text=msg)
48+
logger.warning(msg)
49+
return
50+
51+
return await f(*args, pycamp=pycamp)
52+
return wrap

test/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
from peewee import SqliteDatabase
66
from telegram import Bot
77

8-
from pycamp_bot.models import Pycampista, Slot, Pycamp, WizardAtPycamp
8+
from pycamp_bot.models import Pycampista, Slot, Pycamp, WizardAtPycamp, PycampistaAtPycamp
99

1010

1111
# use an in-memory SQLite for tests.
1212
test_db = SqliteDatabase(':memory:')
1313

14-
MODELS = [Pycampista, Slot, Pycamp, WizardAtPycamp]
14+
MODELS = [Pycampista, Slot, Pycamp, WizardAtPycamp, PycampistaAtPycamp]
1515

1616

1717
def use_test_database(fn):

test/test_pycamp_model.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def test_returns_correct_wizard_within_its_turno(self):
4242
init=datetime(2024,6,20),
4343
end=datetime(2024,6,23),
4444
)
45-
pycamper = Pycampista.create(username="pepe", wizard=True)
45+
pycamper = p.add_wizard("pepe", 123)
4646
wizard.persist_wizards_schedule_in_db(p)
4747

4848
assert p.get_current_wizard() == pycamper

test/test_wizard.py

+42-9
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def teardown_module(module):
2222
# database here. But for tests this is probably not necessary.
2323

2424

25-
class TestWizardScheduleSlots:
25+
class BaseForOtherWizardsTests:
2626

2727
def init_pycamp(self):
2828
self.pycamp = Pycamp.create(
@@ -31,6 +31,8 @@ def init_pycamp(self):
3131
end=datetime(2024,6,24),
3232
)
3333

34+
35+
class TestWizardScheduleSlots(BaseForOtherWizardsTests):
3436
@use_test_database
3537
def test_correct_number_of_slots_in_one_day(self):
3638
p = Pycamp.create(
@@ -95,14 +97,7 @@ def test_no_slot_after_last_day_lunch(self):
9597
assert start >= lunch_time_end
9698

9799

98-
class TestDefineWizardsSchedule:
99-
100-
def init_pycamp(self):
101-
self.pycamp = Pycamp.create(
102-
headquarters="Narnia",
103-
init=datetime(2024,6,20),
104-
end=datetime(2024,6,24),
105-
)
100+
class TestDefineWizardsSchedule(BaseForOtherWizardsTests):
106101

107102
# If no wizards, returns {}
108103
@use_test_database
@@ -164,3 +159,41 @@ def test_all_slots_are_signed_a_wizard(self):
164159
assert all(
165160
(isinstance(s, Pycampista) and s.wizard) for s in sched.values()
166161
)
162+
163+
class TestListWizards(BaseForOtherWizardsTests):
164+
165+
@use_test_database
166+
def test_wizard_registration(self):
167+
self.init_pycamp()
168+
w = self.pycamp.add_wizard("Gandalf", 123)
169+
wizards = self.pycamp.get_wizards()
170+
assert len(wizards) == 1
171+
assert w.username == wizards[0].username
172+
173+
174+
@use_test_database
175+
def test_wizard_registration_works_in_one_pycamp_only(self):
176+
self.init_pycamp()
177+
self.pycamp.add_wizard("Gandalf", 123)
178+
179+
other_pycamp = Pycamp.create(
180+
headquarters="Mordor",
181+
init=datetime(2025,3,22),
182+
end=datetime(2025,3,24),
183+
)
184+
w = other_pycamp.add_wizard("Merlin", 456)
185+
#import ipdb; ipdb.set_trace()
186+
results = other_pycamp.get_wizards()
187+
assert len(results) == 1
188+
assert w.username == results[0].username
189+
190+
191+
# @use_test_database
192+
# def test_no_active_pycamp_then_fail(self):
193+
# self.init_pycamp()
194+
# agregarle un mago
195+
196+
# creo OTRO Pycamp
197+
# agregarle otro mago
198+
199+
# pedir listado: check está el OTRO mago solamente

0 commit comments

Comments
 (0)