Skip to content

Commit

Permalink
Merge branch 'master' into ticket-69
Browse files Browse the repository at this point in the history
  • Loading branch information
WinnaZ authored Jun 22, 2024
2 parents d33b148 + 01c1c7e commit 49393b7
Show file tree
Hide file tree
Showing 19 changed files with 948 additions and 90 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/pythonapp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ on:
pull_request:
branches: [ master ]

env:
TOKEN: foo

jobs:
build:

Expand Down
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ pip install -e .

y estas listo para trabajar.

`pip install freezegun` para correr los tests.

### Python 3.12

`pip install setuptools`

## Testeo

Para correr el bot ejecutá (desde el virtualenv):
Expand Down Expand Up @@ -58,9 +64,8 @@ En este momento ya se puede hablar con el bot. ¿Qué le digo?

* `/su <password>` para reclamar permisos de admin, reemplazando `<password>` por la contraseña que hayamos
elegido en la envvar `PYCAMP_BOT_MASTER_KEY`
* `/agregar_pycamp <pycamp_name>` para crear un pycamp en la deb
* `/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
* `/empezar_pycamp` setea la fecha de inicio del pycamp activo
* `/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 @@ -72,8 +77,22 @@ 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:

1. Todos los candidatos tienen que haberse registrado con `/ser_magx`
2. Tiene que estar creado el schedule de presentaciones de proyectos (`/cronogramear`)

* `/agendar_magx` Asigna un mago por hora durante todo el PyCamp.
* De 9 a 13 y de 14 a 19.
* El primer día arranca después del almuerzo (14hs).
* El último día termina al almuerzo (13hs).

### Flujo pycampista

* `/cargar_proyecto` carga un proyecto (si está habilitada la carga)
* `/votar` envia opciones para votar (si está habilitada la votacion)
* `/ver_cronograma` te muestra el cronograma!
* `/ser_magx` te registra como mago.
* `/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.
33 changes: 33 additions & 0 deletions migrations/migrate_to_wizards_scheduling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# https://docs.peewee-orm.com/en/latest/peewee/playhouse.html#schema-migrations

from datetime import datetime, timedelta
from playhouse.migrate import *
import peewee as pw

from pycamp_bot.models import Pycampista, Slot, Pycamp


my_db = pw.SqliteDatabase('pycamp_projects.db')
migrator = SqliteMigrator(my_db)

from pycamp_bot.models import Pycamp


migrate(
migrator.add_column( # wizard_slot_duration = pw.IntegerField(default=60, null=False)
Pycamp._meta.table_name,
'wizard_slot_duration',
Pycamp.wizard_slot_duration
),
migrator.add_column( # current_wizard = pw.ForeignKeyField(Pycampista)
Slot._meta.table_name,
'current_wizard_id',
Slot.current_wizard
),
)

p = Pycamp.get()
p.end = datetime(2024,6,23,23,59,59,99)
p.end = datetime(2024,6,23,23,59,59,999999)
p.save()

2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
'munch==2.5.0',
'python-telegram-bot==20.2',
'peewee==3.16.0',
'pytest==8.2.2',
'freezegun==1.5.1',
],
test_suite='tests'
)
21 changes: 11 additions & 10 deletions src/pycamp_bot/commands/announcements.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pycamp_bot.commands.auth import get_admins_username
from pycamp_bot.logger import logger
from pycamp_bot.commands.manage_pycamp import active_needed
from pycamp_bot.utils import escape_markdown

PROYECTO, LUGAR, MENSAJE = ["proyecto", "lugar", "mensaje"]

Expand Down Expand Up @@ -78,8 +79,8 @@ async def announce(update: Update, context: CallbackContext) -> str:
if len(_projects) == 0:
await context.bot.send_message(
chat_id=update.message.chat_id,
text=f"No existe el proyecto: *{state.p_name}*.",
parse_mode='Markdown'
text=f"No existe el proyecto: *{escape_markdown(state.p_name)}*.",
parse_mode='MarkdownV2'
)
return ConversationHandler.END
elif not await should_be_able_to_announce(state.username, _projects[0]):
Expand All @@ -92,8 +93,8 @@ async def announce(update: Update, context: CallbackContext) -> str:
else:
await context.bot.send_message(
chat_id=update.message.chat_id,
text=f"Anunciando el proyecto: *{_projects[0].name.capitalize()}* !!!",
parse_mode='Markdown'
text=f"Anunciando el proyecto: *{escape_markdown(_projects[0].name).capitalize()}* !!!",
parse_mode='MarkdownV2'
)
state.owner = _projects[0].owner.username
state.current_project = _projects[0]
Expand Down Expand Up @@ -184,20 +185,20 @@ async def message_project(update: Update, context: CallbackContext) -> str:
try:
await context.bot.send_message(
chat_id=chat_id,
text=f'''Está por empezar el proyecto *"{(state.p_name).capitalize()}"* a cargo de *@{state.owner}*.\n*¿Dónde?* 👉🏼 {state.lugar}''',
parse_mode='Markdown'
text=f'''Está por empezar el proyecto *"{escape_markdown(state.p_name).capitalize()}"* a cargo de *@{escape_markdown(state.owner)}*.\n*¿Dónde?* 👉🏼 {escape_markdown(state.lugar)}''',
parse_mode='MarkdownV2'
)
if update.message.from_user.username == state.owner:
await context.bot.send_message(
chat_id=chat_id,
text=f'*Project Owner says:* **{state.mensaje}**',
parse_mode='Markdown'
text=f'*Project Owner says:* **{escape_markdown(state.mensaje)}**',
parse_mode='MarkdownV2'
)
else:
await context.bot.send_message(
chat_id=chat_id,
text=f'Admin *@{update.message.from_user.username}* says: **{state.mensaje}**',
parse_mode='Markdown'
text=f'Admin *@{escape_markdown(update.message.from_user.username)}* says: **{escape_markdown(state.mensaje)}**',
parse_mode='MarkdownV2'
)
except Exception as e:
logger.error(f"Error al enviar el mensaje: {e}")
Expand Down
1 change: 0 additions & 1 deletion src/pycamp_bot/commands/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ def is_admin(update, context):

def admin_needed(f):
async def wrap(*args, **kargs):
logger.info('Admin nedeed wrapper')
update, context = args
if is_admin(*args):
return await f(*args)
Expand Down
31 changes: 16 additions & 15 deletions src/pycamp_bot/commands/help_msg.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/pycamps: lista todos los pycamps.
/cargar_proyecto: empieza la conversacion de carga de proyecto.
/proyectos: te muestra la informacion de todos los proyectos y sus responsables.
/mis_proyectos: te muestra día y horario de los proyectos que votaste.
/ser_magx: te agrega la lista de Magx.
/evocar_magx: pingea a la/el Magx de turno, informando que necesitas su\
ayuda. Con un gran poder, viene una gran responsabilidad.
Expand All @@ -19,26 +20,26 @@
/ayuda: esta ayuda.'''

HELP_MESSAGE = '''
Este bot facilita la carga, administración y procesamiento de\
Este bot facilita la carga, administración y procesamiento de \
proyectos y votos durante el PyCamp
El proceso se divide en 3 etapas:
*Primera 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\
*Primera 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\
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\
*Segunda 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\
mergear durante las exposiciones (Por tematica similar, u otros\
motivos), y luego se procesan los datos para obtener el cronograma\
*Tercera 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.
''' + user_commands_help
Expand All @@ -49,15 +50,15 @@
Pycamp
------
/agregar_pycamp (pycamp): Agrega un pycamp.
/activar_pycamp (pycamp): Setea un pycamp como activo (si ya hay uno activo lo\
/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.
/terminar_seleccion_proyectos: Deshabilita la seleccion sobre los proyectos del pycamp activo.
/empezar_pycamp: Setea el tiempo de inicio del pycamp activo.\
/empezar_pycamp: Setea el tiempo de inicio del pycamp activo. \
Por default usa datetime.now()
/terminar_pycamp: Setea el timepo de fin del pycamp activo.\
/terminar_pycamp: Setea el timepo de fin del pycamp activo. \
Por default usa datetime.now()
/cronogramear: Te pregunta cuantos dias y que slot tiene tu pycamp \
y genera el cronograma.
Expand Down
113 changes: 90 additions & 23 deletions src/pycamp_bot/commands/manage_pycamp.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import datetime
from telegram.ext import CommandHandler
from telegram.ext import ConversationHandler, CommandHandler, MessageHandler, filters
from pycamp_bot.models import Pycamp
from pycamp_bot.models import Pycampista
from pycamp_bot.models import PycampistaAtPycamp
from pycamp_bot.commands.auth import admin_needed
from pycamp_bot.logger import logger
from pycamp_bot.utils import escape_markdown


SET_DATE_STATE = "set_fate"
SET_DURATION_STATE = "set_duration"
WRAP_UP_STATE = "wrap_up"


def get_pycamp_by_name(name):
Expand Down Expand Up @@ -68,36 +74,101 @@ async def set_active_pycamp(update, context):

@admin_needed
async def add_pycamp(update, context):
parameters = update.message.text.split(' ')
if not len(parameters) == 2:
parameters = update.message.text.split(' ', 1)
if len(parameters) < 2:
await context.bot.send_message(
chat_id=update.message.chat_id,
text="El comando necesita un parametro (pycamp name)")
text="El comando necesita un parametro (headquarters)")
return
hq = parameters[1].strip()
if not hq:
await context.bot.send_message(
chat_id=update.message.chat_id,
text="El parámetro headquarters no puede ser vacío")
return

pycamp = Pycamp.get_or_create(headquarters=parameters[1])[0]
pycamp = Pycamp.get_or_create(headquarters=hq, active=True)[0]
pycamp.set_as_only_active()
logger.info('Creado: {}'.format(pycamp))

msg = "El Pycamp {} fue creado.\n¿Cuándo empieza? (formato yyyy-mm-dd)"
await context.bot.send_message(
chat_id=update.message.chat_id,
text="El Pycamp {} fue creado.".format(pycamp.headquarters))
text=msg.format(pycamp.headquarters)
)
return SET_DATE_STATE


@active_needed
@admin_needed
async def start_pycamp(update, context):
parameters = update.message.text.split(' ')
if len(parameters) == 2:
date = datetime.datetime.fromisoformat(parameters[1])
else:
date = datetime.datetime.now()
async def define_start_date(update, context):
text = update.message.text
try:
start_date = datetime.datetime.fromisoformat(text)
except ValueError:
await context.bot.send_message(
chat_id=update.message.chat_id,
text="mmm no entiendo esa fecha\. El formato esperado es `yyyy-mm-dd`\. ¿De nuevo?",
parse_mode="MarkdownV2"
)
return SET_DATE_STATE

_, pycamp = get_active_pycamp()
pycamp.init = start_date
pycamp.save()

is_active, pycamp = get_active_pycamp()
pycamp.init = date
await context.bot.send_message(
chat_id=update.message.chat_id,
text="¿Cuantos días dura el PyCamp?"
)
return SET_DURATION_STATE


async def define_duration(update, context):
text = update.message.text.strip()
try:
duration = int(text)
except ValueError:
await context.bot.send_message(
chat_id=update.message.chat_id,
text="mmm no entiendo. Poné un número entero porfa."
)
return SET_DURATION_STATE

_, pycamp = get_active_pycamp()
pycamp.end = pycamp.init + datetime.timedelta(
days=duration - 1,
hours=23,
minutes=59,
seconds=59,
milliseconds=99
)
pycamp.save()

msg = "Listo, el PyCamp '{}' está activo, desde el {} hasta el {}".format(
pycamp.headquarters,
pycamp.init.date(),
pycamp.end.date()
)
await context.bot.send_message(
chat_id=update.message.chat_id,
text=msg
)


async def cancel(update, context):
await context.bot.send_message(
chat_id=update.message.chat_id,
text="Empezó Pycamp :) ! {}".format(date))
text="Se canceló la carga del PyCamp...")
return ConversationHandler.END


load_start_pycamp = ConversationHandler(
entry_points=[CommandHandler('empezar_pycamp', add_pycamp)],
states={
SET_DATE_STATE: [MessageHandler(filters.TEXT, define_start_date)],
SET_DURATION_STATE: [MessageHandler(filters.TEXT, define_duration)]
},
fallbacks=[CommandHandler('cancel', cancel)]
)


@active_needed
Expand All @@ -110,6 +181,7 @@ async def end_pycamp(update, context):
date = datetime.datetime.now()

is_active, pycamp = get_active_pycamp()
pycamp.active = False
pycamp.end = date
pycamp.save()

Expand Down Expand Up @@ -162,19 +234,14 @@ async def list_pycampistas(update, context):


def set_handlers(application):
application.add_handler(
CommandHandler('empezar_pycamp', start_pycamp))
application.add_handler(load_start_pycamp)
application.add_handler(
CommandHandler('terminar_pycamp', end_pycamp))
application.add_handler(
CommandHandler('activar_pycamp', set_active_pycamp))
application.add_handler(
CommandHandler('agregar_pycamp', add_pycamp))
application.add_handler(
CommandHandler('pycamps', list_pycamps))
application.add_handler(
CommandHandler('voy_al_pycamp', add_pycampista_to_pycamp))
application.add_handler(
CommandHandler('pycampistas', list_pycampistas))


Loading

0 comments on commit 49393b7

Please sign in to comment.